From a3229efbfafb75d5827e3ad5e42c9837f7a64758 Mon Sep 17 00:00:00 2001 From: Sam Stephenson Date: Mon, 21 Oct 2013 12:03:45 -0500 Subject: [PATCH] Pretty test output for terminals --- README.md | 58 ++++++++----- libexec/bats | 99 +++++++++++++++++---- libexec/bats-format-tap-stream | 154 +++++++++++++++++++++++++++++++++ test/bats.bats | 20 ++++- 4 files changed, 292 insertions(+), 39 deletions(-) create mode 100755 libexec/bats-format-tap-stream diff --git a/README.md b/README.md index 19cdee3..a356ed1 100644 --- a/README.md +++ b/README.md @@ -33,39 +33,48 @@ test passes. In this way, each line is an assertion of truth. ## Running tests 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, and -the results are written to standard output in human-readable [TAP -format](http://testanything.org/wiki/index.php/TAP_specification#THE_TAP_FORMAT). -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. +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. + +When you run Bats from a terminal, you'll see output as each test is +performed, with a check-mark next to the test's name if it passes or +an "X" if it fails. $ bats addition.bats + ✓ addition using bc + ✓ addition using dc + + 2 tests, 0 failures + +If Bats is not connected to a terminal—in other words, if you +run it from a continuous integration system or redirect its output to +a file—the results are displayed in human-readable, machine-parsable +[TAP format](http://testanything.org/wiki/index.php/TAP_specification#THE_TAP_FORMAT). +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 - $ echo $? - 0 - -You can also 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. ### Test suites -You can also invoke the `bats` interpreter 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. +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. ## Helpers and introspection ### The _run_ helper -If you're using Bats, you're probably most interested in testing a -command's 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 @@ -138,7 +147,6 @@ Or you can skip conditionally: ```bash @test "A test which should run" { - if [ foo != bar ]; then skip "foo isn't bar" fi @@ -148,6 +156,12 @@ Or you can skip conditionally: } ``` +### Setup and teardown + +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. + ### Special variables There are several global variables you can use to introspect on Bats @@ -183,7 +197,7 @@ have permission to write to the installation prefix. ## Syntax Highlighting -* [Bats.tmbundle](https://github.com/drnic/Bats.tmbundle) from Dr Nic +* [Bats.tmbundle](https://github.com/drnic/Bats.tmbundle) from Dr Nic Williams adds Bats syntax highlighting support for TextMate. ## Development diff --git a/libexec/bats b/libexec/bats index d7a77fa..a3a00a5 100755 --- a/libexec/bats +++ b/libexec/bats @@ -5,6 +5,27 @@ version() { echo "Bats 0.2.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" } @@ -35,26 +56,61 @@ BATS_LIBEXEC="$(abs_dirname "$0")" export BATS_PREFIX="$(abs_dirname "$BATS_LIBEXEC")" export PATH="$BATS_LIBEXEC:$PATH" -if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then - version - exit 0 -fi +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" + index=$(($index+1)) + done + fi + else + arguments[${#arguments[*]}]="$arg" + fi +done -count_only="" -if [ "$1" = "-c" ]; then - count_only="-c" - shift -fi +unset count_flag pretty +[ -t 0 ] && [ -t 1 ] && pretty="1" -if [ -z "$1" ]; then - { version - echo "usage: $0 [-c] [ ...]" - } >&2 +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 "$@"; do +for filename in "${arguments[@]}"; do if [ -d "$filename" ]; then shopt -s nullglob for suite_filename in "$(expand_path "$filename")"/*.bats; do @@ -67,7 +123,18 @@ for filename in "$@"; do done if [ "${#filenames[@]}" -eq 1 ]; then - exec bats-exec-test $count_only "${filenames[0]}" + command="bats-exec-test" else - exec bats-exec-suite $count_only "${filenames[@]}" + 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-format-tap-stream b/libexec/bats-format-tap-stream new file mode 100755 index 0000000..e5e1a66 --- /dev/null +++ b/libexec/bats-format-tap-stream @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -e + +# Just stream the TAP output (sans extended syntax) if tput is missing +command -v tput >/dev/null || exec grep -v "^begin " + +IFS= read -r header # 1..n +count="${header:3}" +index=0 +failures=0 +name="" +count_column_width=$(( ${#count} * 2 + 2 )) + +update_screen_width() { + screen_width="$(tput cols)" + count_column_left=$(( $screen_width - $count_column_width )) +} + +trap update_screen_width WINCH +update_screen_width + +begin() { + go_to_column 0 + printf_with_truncation $(( $count_column_left - 1 )) " %s" "$name" + clear_to_end_of_line + go_to_column $count_column_left + printf "%${#count}s/${count}" "$index" + go_to_column 1 +} + +pass() { + go_to_column 0 + printf " ✓ %s" "$name" + advance +} + +skip() { + local reason="$1" + [ -z "$reason" ] || reason=": $reason" + go_to_column 0 + printf " - %s (skipped%s)" "$name" "$reason" + advance +} + +fail() { + go_to_column 0 + set_color 1 bold + printf " ✗ %s" "$name" + advance +} + +log() { + set_color 1 + printf " %s\n" "$1" + clear_color +} + +summary() { + printf "\n%d test%s, %d failure%s\n" \ + "$count" "$(plural "$count")" \ + "$failures" "$(plural "$failures")" +} + +printf_with_truncation() { + local width="$1" + shift + local string="$(printf "$@")" + + if [ "${#string}" -gt "$width" ]; then + printf "%s..." "${string:0:$(( $width - 4 ))}" + else + printf "%s" "$string" + fi +} + +go_to_column() { + local column="$1" + tput hpa "$column" +} + +clear_to_end_of_line() { + tput el +} + +advance() { + clear_to_end_of_line + echo + clear_color +} + +set_color() { + local color="$1" + local weight="$2" + tput setaf "$color" + [ -z "$weight" ] || tput "$weight" +} + +clear_color() { + tput sgr0 +} + +plural() { + [ "$1" -eq 1 ] || echo "s" +} + +_buffer="" + +buffer() { + _buffer="${_buffer}$("$@")" +} + +flush() { + printf "%s" "$_buffer" + _buffer="" +} + +finish() { + flush + printf "\n" +} + +trap finish EXIT + +while IFS= read -r line; do + case "$line" in + "begin "* ) + index=$(( $index + 1 )) + name="${line#* $index }" + buffer begin + flush + ;; + "ok "* ) + skip_expr="ok $index # skip (\(([^)]*)\))?" + if [[ "$line" =~ $skip_expr ]]; then + buffer skip "${BASH_REMATCH[2]}" + else + buffer pass + fi + ;; + "not ok "* ) + failures=$(( $failures + 1 )) + buffer fail + ;; + "# "* ) + buffer log "${line:5}" + ;; + "# "* ) + + buffer log "${line:2}" + ;; + esac +done + +buffer summary diff --git a/test/bats.bats b/test/bats.bats index 6747968..2cc13e3 100755 --- a/test/bats.bats +++ b/test/bats.bats @@ -6,7 +6,7 @@ fixtures bats @test "no arguments prints usage instructions" { run bats [ $status -eq 1 ] - [ $(expr "${lines[1]}" : "usage:") -ne 0 ] + [ $(expr "${lines[1]}" : "Usage:") -ne 0 ] } @test "-v and --version print version number" { @@ -15,6 +15,12 @@ fixtures bats [ $(expr "$output" : "Bats [0-9][0-9.]*") -ne 0 ] } +@test "-h and --help print help" { + run bats -h + [ $status -eq 0 ] + [ "${#lines[@]}" -gt 3 ] +} + @test "invalid filename prints an error" { run bats nonexistent [ $status -eq 1 ] @@ -126,3 +132,15 @@ fixtures bats [ "${lines[4]}" = "begin 2 a passing test" ] [ "${lines[5]}" = "ok 2 a passing test" ] } + +@test "pretty and tap formats" { + run bats --tap "$FIXTURE_ROOT/passing.bats" + tap_output="$output" + [ $status -eq 0 ] + + run bats --pretty "$FIXTURE_ROOT/passing.bats" + pretty_output="$output" + [ $status -eq 0 ] + + [ "$tap_output" != "$pretty_output" ] +}