diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..b5823395 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,16 @@ +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' diff --git a/.travis.yml b/.travis.yml index db06d9d7..2007fb4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,34 @@ -language: c -script: bin/bats --tap test +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 diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..71df331b --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +Andrew Martin (https://control-plane.io/) +Bianca Tamayo (https://biancatamayo.me/) +Jason Karns (http://jasonkarns.com/) +Mike Bland (https://mike-bland.com/) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..50a5281a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +ARG bashver=latest + +FROM bash:${bashver} + +RUN ln -s /opt/bats/bin/bats /usr/sbin/bats + +COPY . /opt/bats/ + +ENTRYPOINT ["bash", "/usr/sbin/bats"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index bac4eb29..00000000 --- a/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2014 Sam Stephenson - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..0c742997 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,53 @@ +Copyright (c) 2017 bats-core contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +* [bats-core] is a continuation of [bats]. Copyright for portions of the + bats-core project are held by Sam Stephenson, 2014 as part of the project + [bats], licensed under MIT: + +Copyright (c) 2014 Sam Stephenson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For details, please see the [version control history][commits]. + +[bats-core]: https://github.com/bats-core/bats-core +[bats]:https://github.com/sstephenson/bats +[commits]:https://github.com/bats-core/bats-core/commits/master diff --git a/README.md b/README.md index 235bf1ee..a52ca13b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ -# Bats: Bash Automated Testing System +# Bats-core: Bash Automated Testing System (2018) -Bats is a [TAP](http://testanything.org)-compliant testing framework -for Bash. It provides a simple way to verify that the UNIX programs -you write behave as expected. +[![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) -A Bats test file is a Bash script with special syntax for defining -test cases. Under the hood, each test case is just a function with a -description. +[![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 +way to verify that the UNIX programs you write behave as expected. + +[TAP]: https://testanything.org + +A Bats test file is a Bash script with special syntax for defining test cases. +Under the hood, each test case is just a function with a description. ```bash #!/usr/bin/env bats @@ -22,25 +30,180 @@ description. } ``` -Bats is most useful when testing software written in Bash, but you can -use it to test any UNIX program. +Bats is most useful when testing software written in Bash, but you can use it to +test any UNIX program. + +Test cases consist of standard shell commands. Bats makes use of Bash's +`errexit` (`set -e`) option when running test cases. If every command in the +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. + +[bats-orig]: https://github.com/sstephenson/bats +[0360811]: https://github.com/sstephenson/bats/commit/03608115df2071fff4eaaff1605768c275e5f81f + +## Table of contents + +- [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) +- [Support](#support) +- [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) +- [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 -Test cases consist of standard shell commands. Bats makes use of -Bash's `errexit` (`set -e`) option when running test cases. If every -command in the test case exits with a `0` status code (success), the -test passes. In this way, each line is an assertion of truth. +There is an official image on the Docker Hub: + $ docker run -it bats/bats:latest --version -## Running tests +#### Building a Docker image -To run your tests, invoke the `bats` interpreter with a path to a test -file. The file's test cases 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. +Check out a copy of the Bats repository, then build a container image: -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. + $ 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 `. 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] [ ...] + + 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 @@ -48,27 +211,18 @@ an "X" if it fails. 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](http://testanything.org). +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 -`--tap` option. +You can force TAP output from a terminal by invoking Bats with the `--tap` +option. $ bats --tap addition.bats 1..2 ok 1 addition using bc ok 2 addition using dc -### Test suites - -You can invoke the `bats` interpreter with multiple test file -arguments, or with a path to a directory containing multiple `.bats` -files. Bats will run each test file individually and aggregate the -results. If any test case fails, `bats` exits with a `1` status code. - - ## Writing tests Each Bats test file is evaluated _n+1_ times, where _n_ is the number of @@ -76,21 +230,21 @@ 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](https://github.com/sstephenson/bats/wiki/Bats-Evaluation-Process) -on the wiki. +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. +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. +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" { @@ -100,14 +254,13 @@ an error message. } ``` -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. +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: +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" { @@ -119,23 +272,22 @@ the first line: ### `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 +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. +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. +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" { @@ -170,118 +322,230 @@ Or you can skip conditionally: ### `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 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`. +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: +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_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. +* `$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. +## Support -## Installing Bats from source +The Bats source code repository is [hosted on +GitHub](https://github.com/bats-core/bats-core). There you can file bugs on the +issue tracker or submit tested pull requests for review. -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`, +For real-world examples from open-source projects using Bats, see [Projects +Using Bats](https://github.com/bats-core/bats-core/wiki/Projects-Using-Bats) on +the wiki. - $ git clone https://github.com/sstephenson/bats.git - $ cd bats - $ ./install.sh /usr/local +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. -Note that you may need to run `install.sh` with `sudo` if you do not -have permission to write to the installation prefix. +## Version history +Bats is [SemVer compliant](https://semver.org/). -## Support +*1.1.0* (July 8, 2018) -The Bats source code repository is [hosted on -GitHub](https://github.com/sstephenson/bats). There you can file bugs -on the issue tracker or submit tested pull requests for review. +This is the first release with new features relative to the original Bats 0.4.0. -For real-world examples from open-source projects using Bats, see -[Projects Using Bats](https://github.com/sstephenson/bats/wiki/Projects-Using-Bats) -on the wiki. +Added: +* The `-r, --recursive` flag to scan directory arguments recursively for + `*.bats` files (#109) +* The `contrib/rpm/bats.spec` file to build RPMs (#111) -To learn how to set up your editor for Bats syntax highlighting, see -[Syntax Highlighting](https://github.com/sstephenson/bats/wiki/Syntax-Highlighting) -on the wiki. +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) -## Version history +*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. +* 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. +* 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. +* 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. +* 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 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. +* 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) @@ -289,5 +553,52 @@ on the wiki. --- -© 2014 Sam Stephenson. Bats is released under an MIT-style license; -see `LICENSE` for details. +## Background + +### Why was this fork created? + +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. + +### What's the plan and why? + +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: + +> **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. + +Doing it this way accomplishes a number of things: + +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 + +### Contact us + +- We are `#bats` on freenode + +## Copyright + +© 2018 bats-core organization + +© 2014 Sam Stephenson + +Bats is released under an MIT-style license; see `LICENSE.md` for details. + +[gitter]: https://gitter.im/bats-core/bats-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge diff --git a/bin/bats b/bin/bats deleted file mode 120000 index a50a884e..00000000 --- a/bin/bats +++ /dev/null @@ -1 +0,0 @@ -../libexec/bats \ No newline at end of file diff --git a/bin/bats b/bin/bats new file mode 100755 index 00000000..a852306c --- /dev/null +++ b/bin/bats @@ -0,0 +1,50 @@ +#!/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' +fi + +bats_resolve_link() { + if ! "$BATS_READLINK" "$1"; then + return 0 + fi +} + +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 +} + +export BATS_ROOT +bats_resolve_absolute_root_dir "$0" 'BATS_ROOT' +exec "$BATS_ROOT/libexec/bats-core/bats" "$@" diff --git a/contrib/rpm/bats.spec b/contrib/rpm/bats.spec new file mode 100644 index 00000000..d775900f --- /dev/null +++ b/contrib/rpm/bats.spec @@ -0,0 +1,55 @@ +%global provider github.com +%global project bats-core +%global repo bats-core + +Name: bats +Version: 1.1.0 +Release: 1%{?dist} +Summary: Bash Automated Testing System + +Group: Development/Libraries +License: MIT +URL: https://%{provider}/%{project}/%{repo} +Source0: https://%{provider}/%{project}/%{repo}/archive/v%{version}.tar.gz + +BuildArch: noarch + +Requires: bash + +%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. +Bats is most useful when testing software written in Bash, but you can use it to test any UNIX program. + +%prep +%setup -q -n %{repo}-%{version} + +%install +mkdir -p ${RPM_BUILD_ROOT}%{_prefix} ${RPM_BUILD_ROOT}%{_libexecdir} ${RPM_BUILD_ROOT}%{_mandir} +./install.sh ${RPM_BUILD_ROOT}%{_prefix} + +%clean +rm -rf $RPM_BUILD_ROOT + +%check + +%files +%doc README.md LICENSE.md +%{_bindir}/%{name} +%{_libexecdir}/%{repo} +%{_mandir}/man1/%{name}.1.gz +%{_mandir}/man7/%{name}.7.gz + +%changelog +* Tue Jul 08 2018 mbland - 1.1.0-1 +- Increase version to match upstream release + +* Mon Jun 18 2018 pixdrift - 1.0.2-1 +- Increase version to match upstream release +- Relocate libraries to bats-core subdirectory + +* Sat Jun 09 2018 pixdrift - 1.0.1-1 +- Increase version to match upstream release + +* Fri Jun 08 2018 pixdrift - 1.0.0-1 +- Initial package build of forked (bats-core) github project diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS new file mode 100644 index 00000000..2eb23330 --- /dev/null +++ b/docs/CODEOWNERS @@ -0,0 +1,4 @@ +# This enables automatic code review requests per: +# - https://help.github.com/articles/about-codeowners/ +# - https://help.github.com/articles/enabling-required-reviews-for-pull-requests/ +* @bats-core/bats-core diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d8d6972d --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,92 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting one of the current [project maintainers](#project-maintainers) listed below. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Project Maintainers + +### Current Maintainers + +* [Bianca Tamayo][bt-gh] +* [Mike Bland][mb-gh] +* [Jason Karns][jk-gh] +* [Andrew Martin][am-gh] + +### Past Maintainers + +* Sam Stephenson <> (Original author) + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[bt-gh]: https://github.com/btamayo +[mb-gh]: https://github.com/mbland +[jk-gh]: https://github.com/jasonkarns +[am-gh]: https://github.com/sublimino + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..703e4a8b --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,380 @@ +# Contributing Guidelines + +## Welcome! + +Thank you for considering contributing to the development of this project's +development and/or documentation. Just a reminder: if you're new to this project +or to OSS and want to find issues to work on, please check the following labels +on issues: + +- [help wanted][helpwantedlabel] +- [docs][docslabel] +- [good first issue][goodfirstissuelabel] + +[docslabel]: https://github.com/bats-core/bats-core/labels/docs +[helpwantedlabel]: https://github.com/bats-core/bats-core/labels/help%20wanted +[goodfirstissuelabel]: https://github.com/bats-core/bats-core/labels/good%20first%20issue + +To see all labels and their meanings, [check this wiki page][labelswiki]. + +This guide borrows **heavily** from [@mbland's go-script-bash][gsb] (with some +sections directly quoted), which in turn was +drafted with tips from [Wrangling Web Contributions: How to Build +a CONTRIBUTING.md][moz] and with some inspiration from [the Atom project's +CONTRIBUTING.md file][atom]. + +[gsb]: https://github.com/mbland/go-script-bash/blob/master/CONTRIBUTING.md +[moz]: https://mozillascience.github.io/working-open-workshop/contributing/ +[atom]: https://github.com/atom/atom/blob/master/CONTRIBUTING.md + +[labelswiki]: https://github.com/bats-core/bats-core/wiki/GitHub-Issue-Labels + +## Table of contents + +* [Contributing Guidelines](#contributing-guidelines) + * [Welcome!](#welcome) + * [Table of contents](#table-of-contents) + * [Quick links 🔗](#quick-links-) + * [Contributor License Agreement](#contributor-license-agreement) + * [Code of conduct](#code-of-conduct) + * [Asking questions and reporting issues](#asking-questions-and-reporting-issues) + * [Updating documentation](#updating-documentation) + * [Environment setup](#environment-setup) + * [Workflow](#workflow) + * [Testing](#testing) + * [Coding conventions](#coding-conventions) + * [Formatting](#formatting) + * [Naming](#naming) + * [Function declarations](#function-declarations) + * [Variable and parameter declarations](#variable-and-parameter-declarations) + * [Command substitution](#command-substitution) + * [Process substitution](#process-substitution) + * [Conditionals and loops](#conditionals-and-loops) + * [Generating output](#generating-output) + * [Gotchas](#gotchas) + * [Open Source License](#open-source-license) + * [Credits](#credits) + +## Quick links 🔗 + +- [Gitter channel →][gitterurl]: These messages sync with the IRC channel +- [IRC Channel (#bats on freenode) →][ircurl]: These messages sync with Gitter +- [README →][README] +- [Code of conduct →][CODE_OF_CONDUCT] +- [License information →][LICENSE] +- [Original repository →][repohome] +- [Issues →][repoissues] +- [Pull requests →][repoprs] +- [Milestones →][repomilestones] +- [Projects →][repoprojects] + +[README]: https://github.com/bats-core/bats-core/blob/master/README.md +[CODE_OF_CONDUCT]: https://github.com/bats-core/bats-core/blob/master/docs/CODE_OF_CONDUCT.md +[LICENSE]: https://github.com/bats-core/bats-core/blob/master/LICENSE.md + +## Contributor License Agreement + +Per the [GitHub Terms of Service][gh-tos], be aware that by making a +contribution to this project, you agree: + +* to license your contribution under the same terms as [this project's + license][osmit], and +* that you have the right to license your contribution under those terms. + +See also: ["Does my project need an additional contributor agreement? Probably + not."][cla-needed] + +[gh-tos]: https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license +[osmit]: #open-source-license +[cla-needed]: https://opensource.guide/legal/#does-my-project-need-an-additional-contributor-agreement + + +## Code of conduct + +Harrassment or rudeness of any kind will not be tolerated, period. For +specifics, see the [CODE_OF_CONDUCT][] file. + +## Asking questions and reporting issues + +### Asking questions + +Please check the [README][] or existing [issues][repoissues] first. + +If you cannot find an answer to your question, please feel free to hop on our +[gitter][gitterurl] [![Gitter](https://badges.gitter.im/bats-core/bats-core.svg)](https://gitter.im/bats-core/bats-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) or [via IRC (#bats on freenode)][ircurl]. + +### Reporting issues + +Before reporting an issue, please use the search feature on the [issues +page][repoissues] to see if an issue matching the one you've observed has already +been filed. + +### Updating or filing a new issue + +#### Information to include + +Try to be as specific as possible about your environment and the problem you're +observing. At a minimum, include: + +#### Installation issues + +1. State the version of Bash you're using `bash --version` +1. State your operating system and its version +1. If you're installing through homebrew, run `brew doctor`, and attach the +output of `brew info bats-core` + +#### Bugs/usage issues + +1. State the version of Bash you're using `bash --version` +1. State your operating system and its version +1. Command line steps or code snippets that reproduce the issue +1. Any apparently relevant information from the [Bash changelog][bash-changes] + +[bash-changes]: https://tiswww.case.edu/php/chet/bash/CHANGES + +Also consider using: + +- Bash's `time` builtin to collect running times +- a regression test to add to the suite +- memory usage as reported by a tool such as + [memusg](https://gist.github.com/netj/526585) + +### On existing issues + +1. DO NOT add a +1 comment: Use the reactions provided instead +1. DO add information if you're facing a similar issue to someone else, but +within a different context (e.g. different steps needed to reproduce the issue +than previous stated, different version of Bash or BATS, different OS, etc.) +You can read on how to do that here: [Information to include][#information-to-include] +1. DO remember that you can use the *Subscribe* button on the right side of the +page to receive notifications of further conversations or a resolution. + +## Updating documentation + +We love documentation and people who love documentation! + +If you love writing clear, accessible docs, please don't be shy about pull +requests. Remember: docs are just as important as code. + +Also: _no typo is too small to fix!_ Really. Of course, batches of fixes are +preferred, but even one nit is one nit too many. + +## Environment setup + +Make sure you have Bash installed per the [Environment setup in the +README][env-setup]. + +[env-setup]: https://github.com/bats-core/bats-core/blob/master/README.md#environment-setup + +## Workflow + +The basic workflow for submitting changes resembles that of the [GitHub Git +Flow][github-flow] (a.k.a. GitHub Flow), except that you will be working with +your own fork of the repository and issuing pull requests to the original. + +[github-flow]: https://guides.github.com/introduction/flow/ + +1. Fork the repo on GitHub (look for the "Fork" button) +1. Clone your forked repo to your local machine +1. Create your feature branch (`git checkout -b my-new-feature`) +1. Develop _and [test](#testing)_ your changes as necessary. +1. Commit your changes (`git commit -am 'Add some feature'`) +1. Push to the branch (`git push origin my-new-feature`) +1. Create a new [GitHub pull request][gh-pr] for your feature branch based + against the original repository's `master` branch +1. If your request is accepted, you can [delete your feature branch][rm-branch] + and pull the updated `master` branch from the original repository into your + fork. You may even [delete your fork][rm-fork] if you don't anticipate making + further changes. + +[gh-pr]: https://help.github.com/articles/using-pull-requests/ +[rm-branch]: https://help.github.com/articles/deleting-unused-branches/ +[rm-fork]: https://help.github.com/articles/deleting-a-repository/ + +## 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) + +## Coding conventions + +- [Formatting](#formatting) +- [Naming](#naming) +- [Variable and parameter declarations](#variable-and-parameter-declarations) +- [Command substitution](#command-substitution) +- [Conditions and loops](#conditionals-and-loops) +- [Gotchas](#gotchas) + +### Formatting + +- Keep all files 80 characters wide. +- Indent using two spaces. +- Enclose all variables in double quotes when used to avoid having them + interpreted as glob patterns (unless the variable contains a glob pattern) + and to avoid word splitting when the value contains spaces. Both scenarios + can introduce errors that often prove difficult to diagnose. + - **This is especially important when the variable is used to generate a + glob pattern**, since spaces may appear in a path value. + - If the variable itself contains a glob pattern, make sure to set + `IFS=$'\n'` before using it so that the pattern itself and any matching + file names containing spaces are not split apart. + - Exceptions: Quotes are not required within math contexts, i.e. `(( ))` or + `$(( ))`, and must not be used for variables on the right side of the `=~` + operator. +- Enclose all string literals in single quotes. + - Exception: If the string contains an apostrophe, use double quotes. +- Use quotes around variables and literals even inside of `[[ ]]` conditions. + - This is because strings that contain '[' or ']' characters may fail to + compare equally when they should. + - Exception: Do not quote variables that contain regular expression patterns + appearing on the right side of the `=~` operator. +- _Only_ quote arguments to the right of `=~` if the expression is a literal + match without any metacharacters. + +The following are intended to prevent too-compact code: + +- Declare only one item per `declare`, `local`, `export`, or `readonly` call. + - _Note:_ This also helps avoid subtle bugs, as trying to initialize one + variable using the value of another declared in the same statement will + not do what you may expect. The initialization of the first variable will + not yet be complete when the second variable is declared, so the first + variable will have an empty value. +- Do not use one-line `if`, `for`, `while`, `until`, `case`, or `select` + statements. +- Do not use `&&` or `||` to avoid writing `if` statements. +- Do not write functions entirely on one line. +- For `case` statements: put each pattern on a line by itself; put each command + on a line by itself; put the `;;` terminator on a line by itself. + +### Naming + +- Use `snake_case` for all identifiers. + +### Function declarations + +- Declare functions without the `function` keyword. +- Strive to always use `return`, never `exit`, unless an error condition is + severe enough to warrant it. + - Calling `exit` makes it difficult for the caller to recover from an error, + or to compose new commands from existing ones. + +### Variable and parameter declarations + +- _Gotcha:_ Never initialize an array on the same line as an `export` or + `declare -g` statement. See [the Gotchas section](#gotchas) below for more + details. +- Declare all variables inside functions using `local`. +- Declare temporary file-level variables using `declare`. Use `unset` to remove + them when finished. +- Don't use `local -r`, as a readonly local variable in one scope can cause a + conflict when it calls a function that declares a `local` variable of the same + name. +- Don't use type flags with `declare` or `local`. Assignments to integer + variables in particular may behave differently, and it has no effect on array + variables. +- For most functions, the first lines should use `local` declarations to + assign the original positional parameters to more meaningful names, e.g.: + ```bash + format_summary() { + local cmd_name="$1" + local summary="$2" + local longest_name_len="$3" + ``` + For very short functions, this _may not_ be necessary, e.g.: + ```bash + has_spaces() { + [[ "$1" != "${1//[[:space:]]/}" ]] + } + ``` + +### Command substitution + +- If possible, don't. While this capability is one of Bash's core strengths, + every new process created by Bats makes the framework slower, and speed is + critical to encouraging the practice of automated testing. (This is especially + true on Windows, [where process creation is one or two orders of magnitude + slower][win-slow]. See [bats-core/bats-core#8][pr-8] for an illustration of + the difference avoiding subshells makes.) Bash is quite powerful; see if you + can do what you need in pure Bash first. +- If you need to capture the output from a function, store the output using + `printf -v` instead if possible. `-v` specfies the name of the variable into + which to write the result; the caller can supply this name as a parameter. +- If you must use command substituion, use `$()` instead of backticks, as it's + more robust, more searchable, and can be nested. + +[win-slow]: https://rufflewind.com/2014-08-23/windows-bash-slow +[pr-8]: https://github.com/bats-core/bats-core/pull/8 + +### Process substitution + +- If possible, don't use it. See the advice on avoiding subprocesses and using + `printf -v` in the **Command substitution** section above. +- Use wherever necessary and possible, such as when piping input into a `while` + loop (which avoids having the loop body execute in a subshell) or running a + command taking multiple filename arguments based on output from a function or + pipeline (e.g. `diff`). +- *Warning*: It is impossible to directly determine the exit status of a process + substitution; emitting an exit status as the last line of output is a possible + workaround. + +### Conditionals and loops + +- Always use `[[` and `]]` for evaluating variables. Per the guideline under + **Formatting**, quote variables and strings within the brackets, but not + regular expressions (or variables containing regular expressions) appearing + on the right side of the `=~` operator. + +### Generating output + +- Use `printf` instead of `echo`. Both are Bash builtins, and there's no + perceptible performance difference when running Bats under the `time` builtin. + However, `printf` provides a more consistent experience in general, as `echo` + has limitations to the arguments it accepts, and even the same version of Bash + may produce different results for `echo` based on how the binary was compiled. + See [Stack Overflow: Why is printf better than echo?][printf-vs-echo] for + excruciating details. + +[printf-vs-echo]: https://unix.stackexchange.com/a/65819 + +### Gotchas + +- If you wish to use command substitution to initialize a `local` variable, and + then check the exit status of the command substitution, you _must_ declare the + variable on one line and perform the substitution on another. If you don't, + the exit status will always indicate success, as it is the status of the + `local` declaration, not the command substitution. +- To work around a bug in some versions of Bash whereby arrays declared with + `declare -g` or `export` and initialized in the same statement eventually go + out of scope, always `export` the array name on one line and initialize it the + next line. See: + - https://lists.gnu.org/archive/html/bug-bash/2012-06/msg00068.html + - ftp://ftp.gnu.org/gnu/bash/bash-4.2-patches/bash42-025 + - http://lists.gnu.org/archive/html/help-bash/2012-03/msg00078.html +- [ShellCheck](https://www.shellcheck.net/) can help to identify many of these issues + + +## Open Source License + +This software is made available under the [MIT License][osmit]. +For the text of the license, see the [LICENSE][] file. + +## Credits + +- This guide was heavily written by BATS-core member [@mbland](https://github.com/mbland) +for [go-script-bash](https://github.com/mbland/go-script-bash), tweaked for [BATS-core][repohome] +- Table of Contents created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) +- The [official bash logo](https://github.com/odb/official-bash-logo) is copyrighted +by the [Free Software Foundation](https://www.fsf.org/), 2016 under the [Free Art License](http://artlibre.org/licence/lal/en/) + + + +[repoprojects]: https://github.com/bats-core/bats-core/projects +[repomilestones]: https://github.com/bats-core/bats-core/milestones +[repoprs]: https://github.com/bats-core/bats-core/pulls +[repoissues]: https://github.com/bats-core/bats-core/issues +[repohome]: https://github.com/bats-core/bats-core + +[osmit]: https://opensource.org/licenses/MIT + +[gitterurl]: https://gitter.im/bats-core/bats-core +[ircurl]: https://kiwiirc.com/client/irc.freenode.net:+6697/#bats diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..c7c58d58 --- /dev/null +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +- [ ] I have reviewed the [Contributor Guidelines][contributor]. +- [ ] I have reviewed the [Code of Conduct][coc] and agree to abide by it + +[contributor]: https://github.com/bats-core/bats-core/blob/master/docs/CONTRIBUTING.md +[coc]: https://github.com/bats-core/bats-core/blob/master/docs/CODE_OF_CONDUCT.md diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 00000000..17ef8541 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,58 @@ +# Docker Usage Guide + +- [Docker Usage Guide](#docker-usage-guide) + * [Basic Usage](#basic-usage) + * [Docker Gotchas](#docker-gotchas) + * [Extending from the base image](#extending-from-the-base-image) + +## Basic Usage + +To build and run `bats`' own tests: +```bash +$ git clone https://github.com/bats-core/bats-core.git +Cloning into 'bats-core'... +remote: Counting objects: 1222, done. +remote: Compressing objects: 100% (53/53), done. +remote: Total 1222 (delta 34), reused 55 (delta 21), pack-reused 1146 +Receiving objects: 100% (1222/1222), 327.28 KiB | 1.70 MiB/s, done. +Resolving deltas: 100% (661/661), done. + +$ cd bats-core/ +$ docker build --tag bats:latest . +... +$ docker run -it bats:latest --tap /opt/bats/test +``` + +To mount your tests into the container, first build the image as above. Then, for example with `bats`: +```bash +$ docker run -it -v "$PWD:/opt/bats" bats:latest /opt/bats/test +``` +This runs the `test/` directory from the bats-core repository inside the bats Docker container. + +For test suites that are intended to run in isolation from the project (i.e. the tests do not depend on project files outside of the test directory), you can mount the test directory by itself and execute the tests like so: + +```bash +$ docker run -it -v "$PWD/test:/test" bats:latest /test +``` + +## Docker Gotchas + +Relying on functionality provided by your environment (ssh keys or agent, installed binaries, fixtures outside the mounted test directory) will fail when running inside Docker. + +`--interactive`/`-i` attaches an interactive terminal and is useful to kill hanging processes (otherwise has to be done via docker stop command). `--tty`/`-t` simulates a tty (often not used, but most similar to test runs from a Bash prompt). Interactivity is important to a user, but not a build, and TTYs are probably more important to a headless build. Everything's least-surprising to a new Docker use if both are used. + +## Extending from the base image + +Docker operates on a principle of isolation, and bundles all dependencies required into the Docker image. These can be mounted in at runtime (for test files, configuration, etc). For binary dependencies it may be better to extend the base Docker image with further tools and files. + +```dockerfile +FROM bats + +RUN \ + apk \ + --no-cache \ + --update \ + add \ + openssh + +``` diff --git a/install.sh b/install.sh index 8bbdd16b..30599043 100755 --- a/install.sh +++ b/install.sh @@ -1,37 +1,21 @@ #!/usr/bin/env bash -set -e - -resolve_link() { - $(type -p greadlink readlink | head -1) "$1" -} - -abs_dirname() { - local cwd="$(pwd)" - local path="$1" - while [ -n "$path" ]; do - cd "${path%/*}" - local name="${path##*/}" - path="$(resolve_link "$name" || true)" - done - - pwd - cd "$cwd" -} +set -e +BATS_ROOT="${0%/*}" PREFIX="$1" -if [ -z "$1" ]; then - { echo "usage: $0 " - echo " e.g. $0 /usr/local" - } >&2 + +if [[ -z "$PREFIX" ]]; then + printf '%s\n' \ + "usage: $0 " \ + " e.g. $0 /usr/local" >&2 exit 1 fi -BATS_ROOT="$(abs_dirname "$0")" -mkdir -p "$PREFIX"/{bin,libexec,share/man/man{1,7}} -cp -R "$BATS_ROOT"/bin/* "$PREFIX"/bin -cp -R "$BATS_ROOT"/libexec/* "$PREFIX"/libexec -cp "$BATS_ROOT"/man/bats.1 "$PREFIX"/share/man/man1 -cp "$BATS_ROOT"/man/bats.7 "$PREFIX"/share/man/man7 +install -d -m 755 "$PREFIX"/{bin,libexec/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 644 "$BATS_ROOT/man/bats.1" "$PREFIX/share/man/man1" +install -m 644 "$BATS_ROOT/man/bats.7" "$PREFIX/share/man/man7" echo "Installed Bats to $PREFIX/bin/bats" diff --git a/libexec/bats b/libexec/bats deleted file mode 100755 index 71f392f7..00000000 --- a/libexec/bats +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash -set -e - -version() { - echo "Bats 0.4.0" -} - -usage() { - version - echo "Usage: bats [-c] [-p | -t] [ ...]" -} - -help() { - usage - echo - echo " is the path to a Bats test file, or the path to a directory" - echo " containing Bats test files." - echo - echo " -c, --count Count the number of test cases without running any tests" - echo " -h, --help Display this help message" - echo " -p, --pretty Show results in pretty format (default for terminals)" - echo " -t, --tap Show results in TAP format" - echo " -v, --version Display the version number" - echo - echo " For more information, see https://github.com/sstephenson/bats" - echo -} - -resolve_link() { - $(type -p greadlink readlink | head -1) "$1" -} - -abs_dirname() { - local cwd="$(pwd)" - local path="$1" - - while [ -n "$path" ]; do - cd "${path%/*}" - local name="${path##*/}" - path="$(resolve_link "$name" || true)" - done - - pwd - cd "$cwd" -} - -expand_path() { - { cd "$(dirname "$1")" 2>/dev/null - local dirname="$PWD" - cd "$OLDPWD" - echo "$dirname/$(basename "$1")" - } || echo "$1" -} - -BATS_LIBEXEC="$(abs_dirname "$0")" -export BATS_PREFIX="$(abs_dirname "$BATS_LIBEXEC")" -export BATS_CWD="$(abs_dirname .)" -export PATH="$BATS_LIBEXEC:$PATH" - -options=() -arguments=() -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 - [ -n "$option" ] || break - options[${#options[*]}]="$option" - let index+=1 - done - fi - else - arguments[${#arguments[*]}]="$arg" - fi -done - -unset count_flag pretty -[ -t 0 ] && [ -t 1 ] && pretty="1" -[ -n "$CI" ] && pretty="" - -for option in "${options[@]}"; do - case "$option" in - "h" | "help" ) - help - exit 0 - ;; - "v" | "version" ) - version - exit 0 - ;; - "c" | "count" ) - count_flag="-c" - ;; - "t" | "tap" ) - pretty="" - ;; - "p" | "pretty" ) - pretty="1" - ;; - * ) - usage >&2 - exit 1 - ;; - esac -done - -if [ "${#arguments[@]}" -eq 0 ]; then - usage >&2 - exit 1 -fi - -filenames=() -for filename in "${arguments[@]}"; do - if [ -d "$filename" ]; then - shopt -s nullglob - for suite_filename in "$(expand_path "$filename")"/*.bats; do - filenames["${#filenames[@]}"]="$suite_filename" - done - shopt -u nullglob - else - filenames["${#filenames[@]}"]="$(expand_path "$filename")" - fi -done - -if [ "${#filenames[@]}" -eq 1 ]; then - command="bats-exec-test" -else - command="bats-exec-suite" -fi - -if [ -n "$pretty" ]; then - extended_syntax_flag="-x" - formatter="bats-format-tap-stream" -else - extended_syntax_flag="" - formatter="cat" -fi - -set -o pipefail execfail -exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" | "$formatter" diff --git a/libexec/bats-core/bats b/libexec/bats-core/bats new file mode 100755 index 00000000..256af43e --- /dev/null +++ b/libexec/bats-core/bats @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +set -e + +version() { + printf 'Bats 1.1.0\n' +} + +usage() { + version + printf 'Usage: bats [-c] [-r] [-p | -t] [ ...]\n' +} + +abort() { + printf 'Error: %s\n' "$1" >&2 + usage >&2 + exit 1 +} + +help() { + local line + usage + while read -r line; do + printf '%s\n' "$line" + done < 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 + + For more information, see https://github.com/bats-core/bats-core + +END_OF_HELP_TEXT +} + +expand_path() { + local path="${1%/}" + local dirname="${path%/*}" + local result="$2" + + if [[ "$dirname" == "$path" ]]; then + dirname="$PWD" + else + cd "$dirname" + dirname="$PWD" + cd "$OLDPWD" + fi + printf -v "$result" '%s/%s' "$dirname" "${path##*/}" +} + +export BATS_CWD="$PWD" +export BATS_TEST_PATTERN="^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\{(.*)\$" +export PATH="$BATS_ROOT/libexec/bats-core:$PATH" + +options=() +arguments=() +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 + else + arguments[${#arguments[*]}]="$arg" + fi +done + +unset count_flag pretty recursive +count_flag='' +pretty='' +recursive='' +if [[ -z "${CI:-}" && -t 0 && -t 1 ]]; then + pretty=1 +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="" + ;; + "p" | "pretty" ) + pretty=1 + ;; + * ) + abort "Bad command line option '-$option'" + ;; + esac + done +fi + +if [[ "${#arguments[@]}" -eq 0 ]]; then + abort 'Must specify at least one ' +fi + +filenames=() +for filename in "${arguments[@]}"; do + expand_path "$filename" 'filename' + + if [[ -d "$filename" ]]; then + 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) + else + for suite_filename in "$filename"/*.bats; do + filenames["${#filenames[@]}"]="$suite_filename" + done + fi + shopt -u nullglob + else + filenames["${#filenames[@]}"]="$filename" + fi +done + +if [[ "${#filenames[@]}" -eq 1 ]]; then + command="bats-exec-test" +else + command="bats-exec-suite" +fi + +set -o pipefail execfail +if [[ -z "$pretty" ]]; then + exec "$command" $count_flag "${filenames[@]}" +else + extended_syntax_flag="-x" + formatter="bats-format-tap-stream" + exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" | + "$formatter" +fi diff --git a/libexec/bats-exec-suite b/libexec/bats-core/bats-exec-suite similarity index 50% rename from libexec/bats-exec-suite rename to libexec/bats-core/bats-exec-suite index 29ab255d..4f288273 100755 --- a/libexec/bats-exec-suite +++ b/libexec/bats-core/bats-exec-suite @@ -2,13 +2,13 @@ set -e count_only_flag="" -if [ "$1" = "-c" ]; then +if [[ "$1" = "-c" ]]; then count_only_flag=1 shift fi extended_syntax_flag="" -if [ "$1" = "-x" ]; then +if [[ "$1" = "-x" ]]; then extended_syntax_flag="-x" shift fi @@ -17,15 +17,19 @@ trap "kill 0; exit 1" int count=0 for filename in "$@"; do - let count+="$(bats-exec-test -c "$filename")" + while IFS= read -r line; do + if [[ "$line" =~ $BATS_TEST_PATTERN ]]; then + let count+=1 + fi + done <"$filename" done -if [ -n "$count_only_flag" ]; then - echo "$count" +if [[ -n "$count_only_flag" ]]; then + printf '%d\n' "$count" exit fi -echo "1..$count" +printf '1..%d\n' "$count" status=0 offset=0 for filename in "$@"; do @@ -36,15 +40,19 @@ for filename in "$@"; do case "$line" in "begin "* ) let index+=1 - echo "${line/ $index / $(($offset + $index)) }" + printf '%s\n' "${line/ $index / $(($offset + $index)) }" ;; "ok "* | "not ok "* ) - [ -n "$extended_syntax_flag" ] || let index+=1 - echo "${line/ $index / $(($offset + $index)) }" - [ "${line:0:6}" != "not ok" ] || status=1 + 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 ;; * ) - echo "$line" + printf '%s\n' "$line" ;; esac done diff --git a/libexec/bats-core/bats-exec-test b/libexec/bats-core/bats-exec-test new file mode 100755 index 00000000..f91d521a --- /dev/null +++ b/libexec/bats-core/bats-exec-test @@ -0,0 +1,421 @@ +#!/usr/bin/env bash +set -eET + +BATS_COUNT_ONLY="" +if [[ "$1" = "-c" ]]; then + BATS_COUNT_ONLY=1 + shift +fi + +BATS_EXTENDED_SYNTAX="" +if [[ "$1" = "-x" ]]; then + BATS_EXTENDED_SYNTAX="$1" + shift +fi + +BATS_TEST_FILENAME="$1" +if [[ -z "$BATS_TEST_FILENAME" ]]; then + printf 'usage: bats-exec-test \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 +} + +bats_teardown_trap() { + bats_error_trap + local status=0 + 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 +} + +bats_exit_trap() { + local line + local status + local skipped='' + trap - err exit + + if [[ -n "$BATS_TEST_SKIPPED" ]]; then + skipped=" # skip" + if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then + skipped+=" $BATS_TEST_SKIPPED" + fi + fi + + if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" ]]; then + if [[ "${#BATS_ERROR_STACK_TRACE[@]}" -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 + # errors if they happen during `teardown()` when `bats_perform_test` calls + # `bats_teardown_trap` directly after the test itself passes. + # + # 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_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 + + while IFS= read -r line; do + printf '# %s\n' "$line" + 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" \ + "$skipped" >&3 + status=0 + fi + + rm -f "$BATS_OUT" + 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" +} + +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 + + else + 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" +} + +bats_detect_duplicate_test_case_names() { + local test_names=() + local test_dupes=() + local line + + while read -r line; do + if [[ ! "$line" =~ ^bats_test_function\ ]]; then + continue + fi + line="${line%$'\r'}" + line="${line#* }" + + 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_evaluate_preprocessed_source() { + if [[ -z "$BATS_TEST_SOURCE" ]]; then + BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" + fi + source "$BATS_TEST_SOURCE" +} + +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 diff --git a/libexec/bats-format-tap-stream b/libexec/bats-core/bats-format-tap-stream similarity index 78% rename from libexec/bats-format-tap-stream rename to libexec/bats-core/bats-format-tap-stream index 614768f4..c57defa2 100755 --- a/libexec/bats-format-tap-stream +++ b/libexec/bats-core/bats-format-tap-stream @@ -2,7 +2,9 @@ set -e # Just stream the TAP output (sans extended syntax) if tput is missing -command -v tput >/dev/null || exec grep -v "^begin " +if ! command -v tput >/dev/null; then + exec grep -v "^begin " +fi header_pattern='[0-9]+\.\.[0-9]+' IFS= read -r header @@ -45,7 +47,9 @@ pass() { skip() { local reason="$1" - [ -z "$reason" ] || reason=": $reason" + if [[ -n "$reason" ]]; then + reason=": $reason" + fi go_to_column 0 printf " - %s (skipped%s)" "$name" "$reason" advance @@ -65,11 +69,17 @@ log() { } summary() { - printf "\n%d test%s" "$count" "$(plural "$count")" + printf "\n%d test" "$count" + if [[ "$count" -ne 1 ]]; then + printf 's' + fi - printf ", %d failure%s" "$failures" "$(plural "$failures")" + printf ", %d failure" "$failures" + if [[ "$failures" -ne 1 ]]; then + printf 's' + fi - if [ "$skipped" -gt 0 ]; then + if [[ "$skipped" -gt 0 ]]; then printf ", %d skipped" "$skipped" fi @@ -79,9 +89,11 @@ summary() { printf_with_truncation() { local width="$1" shift - local string="$(printf "$@")" + local string + + printf -v 'string' -- "$@" - if [ "${#string}" -gt "$width" ]; then + if [[ "${#string}" -gt "$width" ]]; then printf "%s..." "${string:0:$(( $width - 4 ))}" else printf "%s" "$string" @@ -99,24 +111,24 @@ clear_to_end_of_line() { advance() { clear_to_end_of_line - echo + printf '\n' clear_color } set_color() { local color="$1" - local weight="$2" - printf "\x1B[%d;%dm" $(( 30 + $color )) "$( [ "$weight" = "bold" ] && echo 1 || echo 22 )" + local weight=22 + + if [[ "$2" == 'bold' ]]; then + weight=1 + fi + printf "\x1B[%d;%dm" $(( 30 + $color )) "$weight" } clear_color() { printf "\x1B[0m" } -plural() { - [ "$1" -eq 1 ] || echo "s" -} - _buffer="" buffer() { @@ -144,7 +156,7 @@ while IFS= read -r line; do flush ;; "ok "* ) - skip_expr="ok $index # skip (\(([^)]*)\))?" + skip_expr="ok $index (.*) # skip ?(([[:print:]]*))?" if [[ "$line" =~ $skip_expr ]]; then let skipped+=1 buffer skip "${BASH_REMATCH[2]}" diff --git a/libexec/bats-preprocess b/libexec/bats-core/bats-preprocess similarity index 58% rename from libexec/bats-preprocess rename to libexec/bats-core/bats-preprocess index 04297ed0..fee41825 100755 --- a/libexec/bats-preprocess +++ b/libexec/bats-core/bats-preprocess @@ -4,6 +4,7 @@ set -e encode_name() { local name="$1" local result="test_" + local hex_code if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then name="${name//_/-5f}" @@ -16,37 +17,39 @@ encode_name() { for ((i=0; i" >&2 - exit 1 -elif [ ! -f "$BATS_TEST_FILENAME" ]; then - echo "bats: $BATS_TEST_FILENAME does not exist" >&2 - exit 1 -else - shift -fi - -BATS_TEST_DIRNAME="$(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 - - [ -f "$filename" ] || { - echo "bats: $filename does not exist" >&2 - exit 1 - } - - source "${filename}" -} - -run() { - local e E T - [[ ! "$-" =~ e ]] || e=1 - [[ ! "$-" =~ E ]] || E=1 - [[ ! "$-" =~ T ]] || T=1 - set +e - set +E - set +T - output="$("$@" 2>&1)" - status="$?" - IFS=$'\n' lines=($output) - [ -z "$e" ] || set -e - [ -z "$E" ] || set -E - [ -z "$T" ] || set -T -} - -setup() { - true -} - -teardown() { - true -} - -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 - echo "begin $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 - fi - setup -} - -bats_test_function() { - local test_name="$1" - BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]="$test_name" -} - -bats_capture_stack_trace() { - BATS_PREVIOUS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) - 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 frame - local index=1 - - while frame="$(caller "$index")"; do - BATS_CURRENT_STACK_TRACE["${#BATS_CURRENT_STACK_TRACE[@]}"]="$frame" - if [[ "$frame" = *"$test_pattern" || \ - "$frame" = *"$setup_pattern" || \ - "$frame" = *"$teardown_pattern" ]]; then - break - else - let index+=1 - fi - done - - BATS_SOURCE="$(bats_frame_filename "${BATS_CURRENT_STACK_TRACE[0]}")" - BATS_LINENO="$(bats_frame_lineno "${BATS_CURRENT_STACK_TRACE[0]}")" -} - -bats_print_stack_trace() { - local frame - local index=1 - local count="${#@}" - - for frame in "$@"; do - local filename="$(bats_trim_filename "$(bats_frame_filename "$frame")")" - local lineno="$(bats_frame_lineno "$frame")" - - if [ $index -eq 1 ]; then - echo -n "# (" - else - echo -n "# " - fi - - local fn="$(bats_frame_function "$frame")" - if [ "$fn" != "$BATS_TEST_NAME" ]; then - echo -n "from function \`$fn' " - fi - - if [ $index -eq $count ]; then - echo "in test file $filename, line $lineno)" - else - echo "in file $filename, line $lineno," - fi - - let index+=1 - done -} - -bats_print_failed_command() { - local frame="$1" - local status="$2" - local filename="$(bats_frame_filename "$frame")" - local lineno="$(bats_frame_lineno "$frame")" - - local failed_line="$(bats_extract_line "$filename" "$lineno")" - local failed_command="$(bats_strip_string "$failed_line")" - echo -n "# \`${failed_command}' " - - if [ $status -eq 1 ]; then - echo "failed" - else - echo "failed with status $status" - fi -} - -bats_frame_lineno() { - local frame="$1" - local lineno="${frame%% *}" - echo "$lineno" -} - -bats_frame_function() { - local frame="$1" - local rest="${frame#* }" - local fn="${rest%% *}" - echo "$fn" -} - -bats_frame_filename() { - local frame="$1" - local rest="${frame#* }" - local filename="${rest#* }" - - if [ "$filename" = "$BATS_TEST_SOURCE" ]; then - echo "$BATS_TEST_FILENAME" - else - echo "$filename" - fi -} - -bats_extract_line() { - local filename="$1" - local lineno="$2" - sed -n "${lineno}p" "$filename" -} - -bats_strip_string() { - local string="$1" - printf "%s" "$string" | sed -e "s/^[ "$'\t'"]*//" -e "s/[ "$'\t'"]*$//" -} - -bats_trim_filename() { - local filename="$1" - local length="${#BATS_CWD}" - - if [ "${filename:0:length+1}" = "${BATS_CWD}/" ]; then - echo "${filename:length+1}" - else - echo "$filename" - fi -} - -bats_debug_trap() { - if [ "$BASH_SOURCE" != "$1" ]; then - bats_capture_stack_trace - fi -} - -bats_error_trap() { - BATS_ERROR_STATUS="$?" - BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" ) - trap - debug -} - -bats_teardown_trap() { - trap "bats_exit_trap" exit - local status=0 - 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 -} - -bats_exit_trap() { - local status - local skipped - trap - err exit - - skipped="" - if [ -n "$BATS_TEST_SKIPPED" ]; then - skipped=" # skip" - if [ "1" != "$BATS_TEST_SKIPPED" ]; then - skipped+=" ($BATS_TEST_SKIPPED)" - fi - fi - - if [ -z "$BATS_TEST_COMPLETED" ] || [ -z "$BATS_TEARDOWN_COMPLETED" ]; then - echo "not ok $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 - sed -e "s/^/# /" < "$BATS_OUT" >&3 - status=1 - else - echo "ok ${BATS_TEST_NUMBER}${skipped} ${BATS_TEST_DESCRIPTION}" >&3 - status=0 - fi - - rm -f "$BATS_OUT" - exit "$status" -} - -bats_perform_tests() { - echo "1..$#" - test_number=1 - status=0 - for test_name in "$@"; do - "$0" $BATS_EXTENDED_SYNTAX "$BATS_TEST_FILENAME" "$test_name" "$test_number" || status=1 - let test_number+=1 - done - exit "$status" -} - -bats_perform_test() { - BATS_TEST_NAME="$1" - if [ "$(type -t "$BATS_TEST_NAME" || true)" = "function" ]; then - BATS_TEST_NUMBER="$2" - if [ -z "$BATS_TEST_NUMBER" ]; then - echo "1..1" - BATS_TEST_NUMBER="1" - fi - - BATS_TEST_COMPLETED="" - BATS_TEARDOWN_COMPLETED="" - 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 - - else - echo "bats: unknown test name \`$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" - { tr -d '\r' < "$BATS_TEST_FILENAME"; echo; } | bats-preprocess > "$BATS_TEST_SOURCE" - trap "bats_cleanup_preprocessed_source" err exit - trap "bats_cleanup_preprocessed_source; exit 1" int -} - -bats_cleanup_preprocessed_source() { - rm -f "$BATS_TEST_SOURCE" -} - -bats_evaluate_preprocessed_source() { - if [ -z "$BATS_TEST_SOURCE" ]; then - BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" - fi - source "$BATS_TEST_SOURCE" -} - -exec 3<&1 - -if [ "$#" -eq 0 ]; then - bats_preprocess_source - bats_evaluate_preprocessed_source - - if [ -n "$BATS_COUNT_ONLY" ]; then - echo "${#BATS_TEST_NAMES[@]}" - else - bats_perform_tests "${BATS_TEST_NAMES[@]}" - fi -else - bats_evaluate_preprocessed_source - bats_perform_test "$@" -fi diff --git a/man/bats.1 b/man/bats.1 index 82f73016..3f94f8b9 100644 --- a/man/bats.1 +++ b/man/bats.1 @@ -45,6 +45,10 @@ Display help message Show results in pretty format (default for terminals) . .TP +\fB\-r\fR, \fB\-\-recursive\fR +Include tests in subdirectories +. +.TP \fB\-t\fR, \fB\-\-tap\fR Show results in TAP format . @@ -89,12 +93,14 @@ ok 2 addition using dc The \fBbats\fR interpreter exits with a value of \fB0\fR if all test cases pass, or \fB1\fR if one or more test cases fail\. . .SH "SEE ALSO" -Bats wiki: \fIhttps://github\.com/sstephenson/bats/wiki/\fR +Bats wiki: \fIhttps://github\.com/bats\-core/bats\-core/wiki/\fR . .P \fBbash\fR(1), \fBbats\fR(7) . .SH "COPYRIGHT" + +(c) 2017 Bianca Tamayo (bats-core organization) (c) 2014 Sam Stephenson . .P diff --git a/man/bats.1.ronn b/man/bats.1.ronn index bd8f45b5..b4903133 100644 --- a/man/bats.1.ronn +++ b/man/bats.1.ronn @@ -52,6 +52,8 @@ OPTIONS Display 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`: @@ -93,7 +95,7 @@ or `1` if one or more test cases fail. SEE ALSO -------- -Bats wiki: _https://github.com/sstephenson/bats/wiki/_ +Bats wiki: _https://github.com/bats\-core/bats\-core/wiki/_ `bash`(1), `bats`(7) @@ -101,6 +103,7 @@ Bats wiki: _https://github.com/sstephenson/bats/wiki/_ COPYRIGHT --------- +(c) 2017 Bianca Tamayo (bats-core organization) (c) 2014 Sam Stephenson Bats is released under the terms of an MIT-style license. diff --git a/package.json b/package.json index 2183feb2..16b4fb78 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,31 @@ { "name": "bats", - "version": "0.3.1", + "version": "1.1.0", "description": "Bash Automated Testing System", - "global": "true", - "install": "./install.sh /usr/local", - "scripts": [ "libexec/bats", "libexec/bats-exec-suite", "libexec/bats-exec-test", "libexec/bats-format-tap-stream", "libexec/bats-preprocess", "bin/bats" ] + "homepage": "https://github.com/bats-core/bats-core#readme", + "license": "MIT", + "author": "Sam Stephenson (http://sstephenson.us/)", + "repository": "github:bats-core/bats-core", + "bugs": "https://github.com/bats-core/bats-core/issues", + "files": [ + "bin", + "libexec", + "man" + ], + "directories": { + "bin": "bin", + "doc": "docs", + "man": "man", + "test": "test" + }, + "scripts": { + "test": "bin/bats test" + }, + "keywords": [ + "bats", + "bash", + "shell", + "test", + "unit" + ] } - diff --git a/test/bats.bats b/test/bats.bats index 280515d2..25270caf 100755 --- a/test/bats.bats +++ b/test/bats.bats @@ -3,10 +3,19 @@ load test_helper fixtures bats -@test "no arguments prints usage instructions" { +@test "no arguments prints message and usage instructions" { run bats [ $status -eq 1 ] - [ $(expr "${lines[1]}" : "Usage:") -ne 0 ] + [ "${lines[0]}" == 'Error: Must specify at least one ' ] + [ "${lines[2]%% *}" == '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:' ] } @test "-v and --version print version number" { @@ -30,40 +39,65 @@ fixtures bats @test "empty test file runs zero tests" { run bats "$FIXTURE_ROOT/empty.bats" [ $status -eq 0 ] - [ $output = "1..0" ] + [ "$output" = "1..0" ] } @test "one passing test" { run bats "$FIXTURE_ROOT/passing.bats" [ $status -eq 0 ] - [ ${lines[0]} = "1..1" ] - [ ${lines[1]} = "ok 1 a passing test" ] + [ "${lines[0]}" = "1..1" ] + [ "${lines[1]}" = "ok 1 a passing test" ] } @test "summary passing tests" { - run filter_control_sequences bats -p $FIXTURE_ROOT/passing.bats + run filter_control_sequences bats -p "$FIXTURE_ROOT/passing.bats" [ $status -eq 0 ] [ "${lines[1]}" = "1 test, 0 failures" ] } @test "summary passing and skipping tests" { - run filter_control_sequences bats -p $FIXTURE_ROOT/passing_and_skipping.bats + run filter_control_sequences bats -p "$FIXTURE_ROOT/passing_and_skipping.bats" + [ $status -eq 0 ] + [ "${lines[3]}" = "3 tests, 0 failures, 2 skipped" ] +} + +@test "tap passing and skipping tests" { + run filter_control_sequences bats --tap "$FIXTURE_ROOT/passing_and_skipping.bats" [ $status -eq 0 ] - [ "${lines[2]}" = "2 tests, 0 failures, 1 skipped" ] + [ "${lines[0]}" = "1..3" ] + [ "${lines[1]}" = "ok 1 a passing test" ] + [ "${lines[2]}" = "ok 2 a skipped test with no reason # skip" ] + [ "${lines[3]}" = "ok 3 a skipped test with a reason # skip for a really good reason" ] } @test "summary passing and failing tests" { - run filter_control_sequences bats -p $FIXTURE_ROOT/failing_and_passing.bats + run filter_control_sequences bats -p "$FIXTURE_ROOT/failing_and_passing.bats" [ $status -eq 0 ] [ "${lines[4]}" = "2 tests, 1 failure" ] } @test "summary passing, failing and skipping tests" { - run filter_control_sequences bats -p $FIXTURE_ROOT/passing_failing_and_skipping.bats + run filter_control_sequences bats -p "$FIXTURE_ROOT/passing_failing_and_skipping.bats" [ $status -eq 0 ] [ "${lines[5]}" = "3 tests, 1 failure, 1 skipped" ] } +@test "tap passing, failing and skipping tests" { + run filter_control_sequences bats --tap "$FIXTURE_ROOT/passing_failing_and_skipping.bats" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..3" ] + [ "${lines[1]}" = "ok 1 a passing test" ] + [ "${lines[2]}" = "ok 2 a skipping test # skip" ] + [ "${lines[3]}" = "not ok 3 a failing test" ] +} + +@test "BATS_CWD is correctly set to PWD as validated by bats_trim_filename" { + local trimmed + bats_trim_filename "$PWD/foo/bar" 'trimmed' + printf 'ACTUAL: %s\n' "$trimmed" >&2 + [ "$trimmed" = 'foo/bar' ] +} + @test "one failing test" { run bats "$FIXTURE_ROOT/failing.bats" [ $status -eq 1 ] @@ -104,18 +138,18 @@ fixtures bats } @test "setup is run once before each test" { - rm -f "$TMP/setup.log" + make_bats_test_suite_tmpdir run bats "$FIXTURE_ROOT/setup.bats" [ $status -eq 0 ] - run cat "$TMP/setup.log" + run cat "$BATS_TEST_SUITE_TMPDIR/setup.log" [ ${#lines[@]} -eq 3 ] } @test "teardown is run once after each test, even if it fails" { - rm -f "$TMP/teardown.log" + make_bats_test_suite_tmpdir run bats "$FIXTURE_ROOT/teardown.bats" [ $status -eq 1 ] - run cat "$TMP/teardown.log" + run cat "$BATS_TEST_SUITE_TMPDIR/teardown.log" [ ${#lines[@]} -eq 3 ] } @@ -140,7 +174,7 @@ fixtures bats [ $status -eq 1 ] [ "${lines[1]}" = 'not ok 1 truth' ] [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 6)" ] - [ "${lines[3]}" = $'# `[ "$PASS" = "1" ]\' failed' ] + [ "${lines[3]}" = $'# `[ "$PASS" = 1 ]\' failed' ] } @test "teardown failure with significant status" { @@ -150,7 +184,8 @@ fixtures bats } @test "failing test file outside of BATS_CWD" { - cd "$TMP" + make_bats_test_suite_tmpdir + cd "$BATS_TEST_SUITE_TMPDIR" run bats "$FIXTURE_ROOT/failing.bats" [ $status -eq 1 ] [ "${lines[2]}" = "# (in test file $FIXTURE_ROOT/failing.bats, line 4)" ] @@ -187,11 +222,11 @@ fixtures bats @test "-c prints the number of tests" { run bats -c "$FIXTURE_ROOT/empty.bats" [ $status -eq 0 ] - [ "$output" = "0" ] + [ "$output" = 0 ] run bats -c "$FIXTURE_ROOT/output.bats" [ $status -eq 0 ] - [ "$output" = "4" ] + [ "$output" = 4 ] } @test "dash-e is not mangled on beginning of line" { @@ -214,8 +249,18 @@ fixtures bats @test "skipped tests" { run bats "$FIXTURE_ROOT/skipped.bats" [ $status -eq 0 ] - [ "${lines[1]}" = "ok 1 # skip a skipped test" ] - [ "${lines[2]}" = "ok 2 # skip (a reason) a skipped test with a reason" ] + [ "${lines[1]}" = "ok 1 a skipped test # skip" ] + [ "${lines[2]}" = "ok 2 a skipped test with a reason # skip a reason" ] +} + +@test "skipped test with parens (pretty formatter)" { + run bats --pretty "$FIXTURE_ROOT/skipped_with_parens.bats" + [ $status -eq 0 ] + + # Some systems (Alpine, for example) seem to emit an extra whitespace into + # entries in the 'lines' array when a carriage return is present from the + # pretty formatter. This is why a '+' is used after the 'skipped' note. + [[ "${lines[@]}" =~ "- a skipped test with parentheses in the reason (skipped: "+"a reason (with parentheses))" ]] } @test "extended syntax" { @@ -256,3 +301,169 @@ fixtures bats [ "${lines[5]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/single_line.bats, line 9)" ] [ "${lines[6]}" = $'# `@test "failing" { false; }\' failed' ] } + +@test "testing IFS not modified by run" { + run bats "$FIXTURE_ROOT/loop_keep_IFS.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 loop_func" ] +} + +@test "expand variables in test name" { + SUITE='test/suite' run bats "$FIXTURE_ROOT/expand_var_in_test_name.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 test/suite: test with variable in name" ] +} + +@test "handle quoted and unquoted test names" { + run bats "$FIXTURE_ROOT/quoted_and_unquoted_test_names.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 single-quoted name" ] + [ "${lines[2]}" = "ok 2 double-quoted name" ] + [ "${lines[3]}" = "ok 3 unquoted name" ] +} + +@test 'ensure compatibility with unofficial Bash strict mode' { + local expected='ok 1 unofficial Bash strict mode conditions met' + + # Run Bats under `set -u` to catch as many unset variable accesses as + # possible. + run bash -u "${BATS_TEST_DIRNAME%/*}/bin/bats" \ + "$FIXTURE_ROOT/unofficial_bash_strict_mode.bats" + if [[ "$status" -ne 0 || "${lines[1]}" != "$expected" ]]; then + cat <&2 + printf 'actual: "%s"\n' "${lines[0]}" >&2 + [ "${lines[0]}" = "$expected" ] + + printf 'num lines: %d\n' "${#lines[*]}" >&2 + [ "${#lines[*]}" = "7" ] +} + +@test "sourcing a nonexistent file in setup produces error output" { + run bats "$FIXTURE_ROOT/source_nonexistent_file_in_setup.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 sourcing nonexistent file fails in setup' ] + [ "${lines[2]}" = "# (from function \`setup' in test file $RELATIVE_FIXTURE_ROOT/source_nonexistent_file_in_setup.bats, line 2)" ] + [ "${lines[3]}" = "# \`source \"nonexistent file\"' failed" ] +} + +@test "referencing unset parameter in setup produces error output" { + run bats "$FIXTURE_ROOT/reference_unset_parameter_in_setup.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 referencing unset parameter fails in setup' ] + [ "${lines[2]}" = "# (from function \`setup' in test file $RELATIVE_FIXTURE_ROOT/reference_unset_parameter_in_setup.bats, line 3)" ] + [ "${lines[3]}" = "# \`echo \"\$unset_parameter\"' failed" ] +} + +@test "sourcing a nonexistent file in test produces error output" { + run bats "$FIXTURE_ROOT/source_nonexistent_file.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 sourcing nonexistent file fails' ] + [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/source_nonexistent_file.bats, line 2)" ] + [ "${lines[3]}" = "# \`source \"nonexistent file\"' failed" ] +} + +@test "referencing unset parameter in test produces error output" { + run bats "$FIXTURE_ROOT/reference_unset_parameter.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 referencing unset parameter fails' ] + [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/reference_unset_parameter.bats, line 3)" ] + [ "${lines[3]}" = "# \`echo \"\$unset_parameter\"' failed" ] +} + +@test "sourcing a nonexistent file in teardown produces error output" { + run bats "$FIXTURE_ROOT/source_nonexistent_file_in_teardown.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 sourcing nonexistent file fails in teardown' ] + [ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/source_nonexistent_file_in_teardown.bats, line 2)" ] + [ "${lines[3]}" = "# \`source \"nonexistent file\"' failed" ] +} + +@test "referencing unset parameter in teardown produces error output" { + run bats "$FIXTURE_ROOT/reference_unset_parameter_in_teardown.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 referencing unset parameter fails in teardown' ] + [ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/reference_unset_parameter_in_teardown.bats, line 3)" ] + [ "${lines[3]}" = "# \`echo \"\$unset_parameter\"' failed" ] +} + +@test "execute exported function without breaking failing test output" { + exported_function() { return 0; } + export -f exported_function + run bats "$FIXTURE_ROOT/exported_function.bats" + [ $status -eq 1 ] + [ "${lines[0]}" = "1..1" ] + [ "${lines[1]}" = "not ok 1 failing test" ] + [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/exported_function.bats, line 7)" ] + [ "${lines[3]}" = "# \`false' failed" ] + [ "${lines[4]}" = "# a='exported_function'" ] +} + +@test "output printed even when no final newline" { + run bats "$FIXTURE_ROOT/no-final-newline.bats" + printf 'num lines: %d\n' "${#lines[@]}" >&2 + printf 'LINE: %s\n' "${lines[@]}" >&2 + [ "$status" -eq 1 ] + [ "${#lines[@]}" -eq 7 ] + [ "${lines[1]}" = 'not ok 1 no final newline' ] + [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/no-final-newline.bats, line 2)" ] + [ "${lines[3]}" = "# \`printf 'foo\nbar\nbaz' >&2 && return 1' failed" ] + [ "${lines[4]}" = '# foo' ] + [ "${lines[5]}" = '# bar' ] + [ "${lines[6]}" = '# baz' ] +} diff --git a/test/fixtures/bats/duplicate-tests.bats b/test/fixtures/bats/duplicate-tests.bats new file mode 100644 index 00000000..09ec24ed --- /dev/null +++ b/test/fixtures/bats/duplicate-tests.bats @@ -0,0 +1,13 @@ +# This does not fail as expected +@test "gizmo test" { + false +} + +@test "gizmo test" "this does fail, as expected" { + false +} + +# This overrides any previous test from the suite with the same description +@test "gizmo test" { + true +} diff --git a/test/fixtures/bats/expand_var_in_test_name.bats b/test/fixtures/bats/expand_var_in_test_name.bats new file mode 100644 index 00000000..8f6dec23 --- /dev/null +++ b/test/fixtures/bats/expand_var_in_test_name.bats @@ -0,0 +1,3 @@ +@test "$SUITE: test with variable in name" { + true +} diff --git a/test/fixtures/bats/exported_function.bats b/test/fixtures/bats/exported_function.bats new file mode 100644 index 00000000..6f300863 --- /dev/null +++ b/test/fixtures/bats/exported_function.bats @@ -0,0 +1,8 @@ +if exported_function; then + a='exported_function' +fi + +@test "failing test" { + echo "a='$a'" + false +} diff --git a/test/fixtures/bats/failing_teardown.bats b/test/fixtures/bats/failing_teardown.bats index 28eebf6f..8bda992d 100644 --- a/test/fixtures/bats/failing_teardown.bats +++ b/test/fixtures/bats/failing_teardown.bats @@ -3,5 +3,5 @@ teardown() { } @test "truth" { - [ "$PASS" = "1" ] + [ "$PASS" = 1 ] } diff --git a/test/fixtures/bats/loop_keep_IFS.bats b/test/fixtures/bats/loop_keep_IFS.bats new file mode 100644 index 00000000..f30613ae --- /dev/null +++ b/test/fixtures/bats/loop_keep_IFS.bats @@ -0,0 +1,16 @@ +# see issue #89 +loop_func() { + local search="none one two tree" + local d + + for d in $search ; do + echo $d + done +} + +@test "loop_func" { + run loop_func + [[ "${lines[3]}" == 'tree' ]] + run loop_func + [[ "${lines[2]}" == 'two' ]] +} diff --git a/test/fixtures/bats/no-final-newline.bats b/test/fixtures/bats/no-final-newline.bats new file mode 100644 index 00000000..646b86a2 --- /dev/null +++ b/test/fixtures/bats/no-final-newline.bats @@ -0,0 +1,3 @@ +@test "no final newline" { + printf 'foo\nbar\nbaz' >&2 && return 1 +} diff --git a/test/fixtures/bats/passing_and_skipping.bats b/test/fixtures/bats/passing_and_skipping.bats index 88d74bef..83fc76bf 100644 --- a/test/fixtures/bats/passing_and_skipping.bats +++ b/test/fixtures/bats/passing_and_skipping.bats @@ -2,6 +2,10 @@ true } -@test "a skipping test" { +@test "a skipped test with no reason" { skip } + +@test "a skipped test with a reason" { + skip "for a really good reason" +} diff --git a/test/fixtures/bats/quoted_and_unquoted_test_names.bats b/test/fixtures/bats/quoted_and_unquoted_test_names.bats new file mode 100644 index 00000000..aa460daa --- /dev/null +++ b/test/fixtures/bats/quoted_and_unquoted_test_names.bats @@ -0,0 +1,11 @@ +@test 'single-quoted name' { + true +} + +@test "double-quoted name" { + true +} + +@test unquoted name { + true +} diff --git a/test/fixtures/bats/reference_unset_parameter.bats b/test/fixtures/bats/reference_unset_parameter.bats new file mode 100644 index 00000000..9df02ffb --- /dev/null +++ b/test/fixtures/bats/reference_unset_parameter.bats @@ -0,0 +1,4 @@ +@test "referencing unset parameter fails" { + set -u + echo "$unset_parameter" +} diff --git a/test/fixtures/bats/reference_unset_parameter_in_setup.bats b/test/fixtures/bats/reference_unset_parameter_in_setup.bats new file mode 100644 index 00000000..0a2764b5 --- /dev/null +++ b/test/fixtures/bats/reference_unset_parameter_in_setup.bats @@ -0,0 +1,13 @@ +setup() { + set -u + echo "$unset_parameter" +} + +teardown() { + echo "should not capture the next line" + [ 1 -eq 2 ] +} + +@test "referencing unset parameter fails in setup" { + : +} diff --git a/test/fixtures/bats/reference_unset_parameter_in_teardown.bats b/test/fixtures/bats/reference_unset_parameter_in_teardown.bats new file mode 100644 index 00000000..79f79ee0 --- /dev/null +++ b/test/fixtures/bats/reference_unset_parameter_in_teardown.bats @@ -0,0 +1,8 @@ +teardown() { + set -u + echo "$unset_parameter" +} + +@test "referencing unset parameter fails in teardown" { + : +} diff --git a/test/fixtures/bats/setup.bats b/test/fixtures/bats/setup.bats index 3cc52cc7..0fc9eea6 100644 --- a/test/fixtures/bats/setup.bats +++ b/test/fixtures/bats/setup.bats @@ -1,4 +1,4 @@ -LOG="$TMP/setup.log" +LOG="$BATS_TEST_SUITE_TMPDIR/setup.log" setup() { echo "$BATS_TEST_NAME" >> "$LOG" diff --git a/test/fixtures/bats/skipped_with_parens.bats b/test/fixtures/bats/skipped_with_parens.bats new file mode 100644 index 00000000..3f8f6283 --- /dev/null +++ b/test/fixtures/bats/skipped_with_parens.bats @@ -0,0 +1,3 @@ +@test "a skipped test with parentheses in the reason" { + skip "a reason (with parentheses)" +} diff --git a/test/fixtures/bats/source_nonexistent_file.bats b/test/fixtures/bats/source_nonexistent_file.bats new file mode 100644 index 00000000..0f02c17a --- /dev/null +++ b/test/fixtures/bats/source_nonexistent_file.bats @@ -0,0 +1,3 @@ +@test "sourcing nonexistent file fails" { + source "nonexistent file" +} diff --git a/test/fixtures/bats/source_nonexistent_file_in_setup.bats b/test/fixtures/bats/source_nonexistent_file_in_setup.bats new file mode 100644 index 00000000..c37e73b6 --- /dev/null +++ b/test/fixtures/bats/source_nonexistent_file_in_setup.bats @@ -0,0 +1,12 @@ +setup() { + source "nonexistent file" +} + +teardown() { + echo "should not capture the next line" + [ 1 -eq 2 ] +} + +@test "sourcing nonexistent file fails in setup" { + : +} diff --git a/test/fixtures/bats/source_nonexistent_file_in_teardown.bats b/test/fixtures/bats/source_nonexistent_file_in_teardown.bats new file mode 100644 index 00000000..d9087f16 --- /dev/null +++ b/test/fixtures/bats/source_nonexistent_file_in_teardown.bats @@ -0,0 +1,7 @@ +teardown() { + source "nonexistent file" +} + +@test "sourcing nonexistent file fails in teardown" { + : +} diff --git a/test/fixtures/bats/teardown.bats b/test/fixtures/bats/teardown.bats index 678e0f7e..2fde1e14 100644 --- a/test/fixtures/bats/teardown.bats +++ b/test/fixtures/bats/teardown.bats @@ -1,4 +1,4 @@ -LOG="$TMP/teardown.log" +LOG="$BATS_TEST_SUITE_TMPDIR/teardown.log" teardown() { echo "$BATS_TEST_NAME" >> "$LOG" diff --git a/test/fixtures/bats/unofficial_bash_strict_mode.bash b/test/fixtures/bats/unofficial_bash_strict_mode.bash new file mode 100644 index 00000000..4c57a714 --- /dev/null +++ b/test/fixtures/bats/unofficial_bash_strict_mode.bash @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' diff --git a/test/fixtures/bats/unofficial_bash_strict_mode.bats b/test/fixtures/bats/unofficial_bash_strict_mode.bats new file mode 100644 index 00000000..473f3e46 --- /dev/null +++ b/test/fixtures/bats/unofficial_bash_strict_mode.bats @@ -0,0 +1,4 @@ +load unofficial_bash_strict_mode +@test "unofficial Bash strict mode conditions met" { + : +} diff --git a/test/fixtures/bats/whitespace.bats b/test/fixtures/bats/whitespace.bats new file mode 100644 index 00000000..15740857 --- /dev/null +++ b/test/fixtures/bats/whitespace.bats @@ -0,0 +1,33 @@ +@test "no extra whitespace" { + : +} + + @test "tab at beginning of line" { + : + } + +@test "tab before description" { + : +} + +@test "tab before opening brace" { + : +} + + @test "tabs at beginning of line and before description" { + : + } + + @test "tabs at beginning, before description, before brace" { + : + } + + @test "extra whitespace around single-line test" { :; } + +@test "no extra whitespace around single-line test" {:;} + +@test parse unquoted name between extra whitespace {:;} + +@test { {:;} # unquote single brace is a valid description + +@test ' {:;} # empty name from single quote diff --git a/test/fixtures/suite/recursive/subsuite/test2.bats b/test/fixtures/suite/recursive/subsuite/test2.bats new file mode 100644 index 00000000..9fb62c41 --- /dev/null +++ b/test/fixtures/suite/recursive/subsuite/test2.bats @@ -0,0 +1,3 @@ +@test "another passing test" { + true +} diff --git a/test/fixtures/suite/recursive/test.bats b/test/fixtures/suite/recursive/test.bats new file mode 100644 index 00000000..e8182ce0 --- /dev/null +++ b/test/fixtures/suite/recursive/test.bats @@ -0,0 +1,3 @@ +@test "a passing test" { + true +} diff --git a/test/install.bats b/test/install.bats new file mode 100644 index 00000000..f16be138 --- /dev/null +++ b/test/install.bats @@ -0,0 +1,68 @@ +#!/usr/bin/env bats + +load test_helper + +INSTALL_DIR= +BATS_ROOT= + +setup() { + make_bats_test_suite_tmpdir + INSTALL_DIR="$BATS_TEST_SUITE_TMPDIR/bats-core" + BATS_ROOT="${BATS_TEST_DIRNAME%/*}" +} + +@test "install.sh creates a valid installation" { + run "$BATS_ROOT/install.sh" "$INSTALL_DIR" + emit_debug_output + [ "$status" -eq 0 ] + [ "$output" == "Installed Bats to $INSTALL_DIR/bin/bats" ] + [ -x "$INSTALL_DIR/bin/bats" ] + [ -x "$INSTALL_DIR/libexec/bats-core/bats" ] + [ -x "$INSTALL_DIR/libexec/bats-core/bats-exec-suite" ] + [ -x "$INSTALL_DIR/libexec/bats-core/bats-exec-test" ] + [ -x "$INSTALL_DIR/libexec/bats-core/bats-format-tap-stream" ] + [ -x "$INSTALL_DIR/libexec/bats-core/bats-preprocess" ] + [ -f "$INSTALL_DIR/share/man/man1/bats.1" ] + [ -f "$INSTALL_DIR/share/man/man7/bats.7" ] + + run "$INSTALL_DIR/bin/bats" -v + [ "$status" -eq 0 ] + [ "${output%% *}" == 'Bats' ] +} + +@test "install.sh only updates permissions for Bats files" { + mkdir -p "$INSTALL_DIR"/{bin,libexec/bats-core} + + local dummy_bin="$INSTALL_DIR/bin/dummy" + printf 'dummy' >"$dummy_bin" + + local dummy_libexec="$INSTALL_DIR/libexec/bats-core/dummy" + printf 'dummy' >"$dummy_libexec" + + run "$BATS_ROOT/install.sh" "$INSTALL_DIR" + [ "$status" -eq 0 ] + [ -f "$dummy_bin" ] + [ ! -x "$dummy_bin" ] + [ -f "$dummy_libexec" ] + [ ! -x "$dummy_libexec" ] +} + +@test "bin/bats is resilient to symbolic links" { + run "$BATS_ROOT/install.sh" "$INSTALL_DIR" + [ "$status" -eq 0 ] + + # Simulate a symlink to bin/bats (without using a symlink, for Windows sake) + # by creating a wrapper script that executes bin/bats via a relative path. + # + # root.bats contains tests that use real symlinks on platforms that support + # them, as does the .travis.yml script that exercises the Dockerfile. + local bats_symlink="$INSTALL_DIR/bin/bats-link" + printf '%s\n' '#! /usr/bin/env bash' \ + "cd '$INSTALL_DIR/bin'" \ + './bats "$@"' >"$bats_symlink" + chmod 700 "$bats_symlink" + + run "$bats_symlink" -v + [ "$status" -eq 0 ] + [ "${output%% *}" == 'Bats' ] +} diff --git a/test/root.bats b/test/root.bats new file mode 100644 index 00000000..574e0056 --- /dev/null +++ b/test/root.bats @@ -0,0 +1,67 @@ +#!/usr/bin/env bats +# +# This suite is dedicated to calculating BATS_ROOT when going through various +# permutations of symlinks. It was inspired by the report in issue #113 that the +# calculation was broken on CentOS, where /bin is symlinked to /usr/bin. +# +# The basic test environment is (all paths relative to BATS_TEST_SUITE_TMPDIR): +# +# - /bin is a relative symlink to /usr/bin, exercising the symlink resolution of +# the `bats` parent directory (i.e. "${0%/*}") +# - /usr/bin/bats is an absolute symlink to /opt/bats-core/bin/bats, exercising +# the symlink resolution of the `bats` executable itself (i.e. "${0##*/}") + +load test_helper + +# This would make a good candidate for a one-time setup/teardown per #39. +setup() { + make_bats_test_suite_tmpdir + cd "$BATS_TEST_SUITE_TMPDIR" + mkdir -p {usr/bin,opt/bats-core} + "$BATS_ROOT/install.sh" "opt/bats-core" + + ln -s "usr/bin" "bin" + + if [[ ! -L "bin" ]]; then + cd - >/dev/null + skip "symbolic links aren't functional on OSTYPE=$OSTYPE" + fi + + ln -s "$BATS_TEST_SUITE_TMPDIR/opt/bats-core/bin/bats" \ + "$BATS_TEST_SUITE_TMPDIR/usr/bin/bats" + cd - >/dev/null +} + +@test "#113: set BATS_ROOT when /bin is a symlink to /usr/bin" { + run "$BATS_TEST_SUITE_TMPDIR/bin/bats" -v + [ "$status" -eq 0 ] + [ "${output%% *}" == 'Bats' ] +} + +# The resolution scheme here is: +# +# - /bin => /usr/bin (relative directory) +# - /usr/bin/foo => /usr/bin/bar (relative executable) +# - /usr/bin/bar => /opt/bats/bin0/bar (absolute executable) +# - /opt/bats/bin0 => /opt/bats/bin1 (relative directory) +# - /opt/bats/bin1 => /opt/bats/bin2 (absolute directory) +# - /opt/bats/bin2/bar => /opt/bats-core/bin/bar (absolute executable) +# - /opt/bats-core/bin/bar => /opt/bats-core/bin/baz (relative executable) +# - /opt/bats-core/bin/baz => /opt/bats-core/bin/bats (relative executable) +@test "set BATS_ROOT with extreme symlink resolution" { + cd "$BATS_TEST_SUITE_TMPDIR" + mkdir -p "opt/bats/bin2" + + ln -s bar usr/bin/foo + ln -s "$BATS_TEST_SUITE_TMPDIR/opt/bats/bin0/bar" usr/bin/bar + ln -s bin1 opt/bats/bin0 + ln -s "$BATS_TEST_SUITE_TMPDIR/opt/bats/bin2" opt/bats/bin1 + ln -s "$BATS_TEST_SUITE_TMPDIR/opt/bats-core/bin/bar" opt/bats/bin2/bar + ln -s baz opt/bats-core/bin/bar + ln -s bats opt/bats-core/bin/baz + + cd - >/dev/null + run "$BATS_TEST_SUITE_TMPDIR/bin/foo" -v + [ "$status" -eq 0 ] + [ "${output%% *}" == 'Bats' ] +} diff --git a/test/suite.bats b/test/suite.bats index 14f5008e..cc4e4d30 100755 --- a/test/suite.bats +++ b/test/suite.bats @@ -6,30 +6,30 @@ fixtures suite @test "running a suite with no test files" { run bats "$FIXTURE_ROOT/empty" [ $status -eq 0 ] - [ $output = "1..0" ] + [ "$output" = "1..0" ] } @test "running a suite with one test file" { run bats "$FIXTURE_ROOT/single" [ $status -eq 0 ] - [ ${lines[0]} = "1..1" ] - [ ${lines[1]} = "ok 1 a passing test" ] + [ "${lines[0]}" = "1..1" ] + [ "${lines[1]}" = "ok 1 a passing test" ] } @test "counting tests in a suite" { run bats -c "$FIXTURE_ROOT/single" [ $status -eq 0 ] - [ $output -eq 1 ] + [ "$output" -eq 1 ] run bats -c "$FIXTURE_ROOT/multiple" [ $status -eq 0 ] - [ $output -eq 3 ] + [ "$output" -eq 3 ] } @test "aggregated output of multiple tests in a suite" { run bats "$FIXTURE_ROOT/multiple" [ $status -eq 0 ] - [ ${lines[0]} = "1..3" ] + [ "${lines[0]}" = "1..3" ] echo "$output" | grep "^ok . truth" echo "$output" | grep "^ok . more truth" echo "$output" | grep "^ok . quasi-truth" @@ -38,14 +38,14 @@ fixtures suite @test "a failing test in a suite results in an error exit code" { FLUNK=1 run bats "$FIXTURE_ROOT/multiple" [ $status -eq 1 ] - [ ${lines[0]} = "1..3" ] + [ "${lines[0]}" = "1..3" ] echo "$output" | grep "^not ok . quasi-truth" } @test "running an ad-hoc suite by specifying multiple test files" { run bats "$FIXTURE_ROOT/multiple/a.bats" "$FIXTURE_ROOT/multiple/b.bats" [ $status -eq 0 ] - [ ${lines[0]} = "1..3" ] + [ "${lines[0]}" = "1..3" ] echo "$output" | grep "^ok . truth" echo "$output" | grep "^ok . more truth" echo "$output" | grep "^ok . quasi-truth" @@ -62,3 +62,19 @@ fixtures suite [ "${lines[5]}" = "begin 3 quasi-truth" ] [ "${lines[6]}" = "not ok 3 quasi-truth" ] } + +@test "recursive support (short option)" { + run bats -r "${FIXTURE_ROOT}/recursive" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..2" ] + [ "${lines[1]}" = "ok 1 another passing test" ] + [ "${lines[2]}" = "ok 2 a passing test" ] +} + +@test "recursive support (long option)" { + run bats --recursive "${FIXTURE_ROOT}/recursive" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..2" ] + [ "${lines[1]}" = "ok 1 another passing test" ] + [ "${lines[2]}" = "ok 2 a passing test" ] +} diff --git a/test/test_helper.bash b/test/test_helper.bash index 84eee8c3..ef798197 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -1,16 +1,30 @@ fixtures() { FIXTURE_ROOT="$BATS_TEST_DIRNAME/fixtures/$1" - RELATIVE_FIXTURE_ROOT="$(bats_trim_filename "$FIXTURE_ROOT")" + bats_trim_filename "$FIXTURE_ROOT" 'RELATIVE_FIXTURE_ROOT' } -setup() { - export TMP="$BATS_TEST_DIRNAME/tmp" +make_bats_test_suite_tmpdir() { + export BATS_TEST_SUITE_TMPDIR="$BATS_TMPDIR/bats-test-tmp" + mkdir -p "$BATS_TEST_SUITE_TMPDIR" } filter_control_sequences() { "$@" | sed $'s,\x1b\\[[0-9;]*[a-zA-Z],,g' } +if ! command -v tput >/dev/null; then + tput() { + printf '1000\n' + } + export -f tput +fi + +emit_debug_output() { + printf '%s\n' 'output:' "$output" >&2 +} + teardown() { - [ -d "$TMP" ] && rm -f "$TMP"/* + if [[ -n "$BATS_TEST_SUITE_TMPDIR" ]]; then + rm -rf "$BATS_TEST_SUITE_TMPDIR" + fi } diff --git a/test/tmp/.gitignore b/test/tmp/.gitignore deleted file mode 100644 index 241e560d..00000000 --- a/test/tmp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -