Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/bats-core/bats-support.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZoltan Tombol <zoltan.tombol@gmail.com>2016-02-10 03:13:29 +0300
committerZoltan Tombol <zoltan.tombol@gmail.com>2016-02-10 03:22:53 +0300
commit2c97639223e65e54bec2bccda788ba3db338fc22 (patch)
treefe0d12d185a008d1e8af3c60d27e1afd55fc6378
Initial commit
-rw-r--r--COPYING121
-rw-r--r--README.md115
-rw-r--r--load.bash1
-rw-r--r--src/output.bash279
-rwxr-xr-xtest/50-output-10-batslib_err.bats16
-rwxr-xr-xtest/50-output-11-batslib_count_lines.bats21
-rwxr-xr-xtest/50-output-12-batslib_is_single_line.bats13
-rwxr-xr-xtest/50-output-13-batslib_get_max_single_line_key_width.bats21
-rwxr-xr-xtest/50-output-14-batslib_print_kv_single.bats27
-rwxr-xr-xtest/50-output-15-batslib_print_kv_multi.bats19
-rwxr-xr-xtest/50-output-16-batslib_print_kv_single_or_multi.bats31
-rwxr-xr-xtest/50-output-17-batslib_prefix.bats43
-rwxr-xr-xtest/50-output-18-batslib_mark.bats72
-rwxr-xr-xtest/50-output-19-batslib_decorate.bats13
-rw-r--r--test/test_helper.bash6
15 files changed, 798 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ef9dc0d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,115 @@
+# bats-core
+
+`bats-core` is a supporting library providing common functions to test
+helper libraries written for [Bats][bats].
+
+Features:
+- [output formatting](#output-formatting)
+
+See the [shared documentation][bats-docs] to learn how to install and
+load this library.
+
+If you want to use this library in your own helpers or just want to
+learn about its internals see the developer documentation in the [source
+files](src).
+
+
+## Output formatting
+
+Many test helpers need to produce human readable output. This library
+provides a simple way to format simple messages and key value pairs, and
+display them on the standard error.
+
+
+### Simple message
+
+Simple messages without structure, e.g. one-line error messages, are
+simply wrapped in a header and a footer to help them stand out.
+
+```
+-- ERROR: assert_output --
+`--partial' and `--regexp' are mutually exclusive
+--
+```
+
+
+### Key-Value pairs
+
+Some helpers, e.g. [assertions][bats-assert], structure output as
+key-value pairs. This library provides two ways to format them.
+
+When the value is one line long, a pair can be displayed in a columnar
+fashion called ***two-column*** format.
+
+```
+-- output differs --
+expected : want
+actual : have
+--
+```
+
+When the value is longer than one line, the key and value must be
+displayed on separate lines. First, the key is displayed along with the
+number of lines in the value. Then, the value, indented by two spaces
+for added readability, starting on the next line. This is called
+***multi-line*** format.
+
+```
+-- command failed --
+status : 1
+output (2 lines):
+ Error! Something went terribly wrong!
+ Our engineers are panicing... \`>`;/
+--
+```
+
+Sometimes, for clarity, it is a good idea to display related values also
+in this format, even if they are just one line long.
+
+```
+-- output differs --
+expected (1 lines):
+ want
+actual (3 lines):
+ have 1
+ have 2
+ have 3
+--
+```
+
+
+# Licence
+
+`bats-core` is licensed under the CC0 licence to ensure the highest
+possible compatibility with other software.
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+<!-- BEGIN GENERATED with https://creativecommons.org/choose/zero/ -->
+
+<p xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#">
+ <a rel="license"
+ href="http://creativecommons.org/publicdomain/zero/1.0/">
+ <img src="http://i.creativecommons.org/p/zero/1.0/88x31.png" style="border-style: none;" alt="CC0" />
+ </a>
+ <br />
+ To the extent possible under law,
+ <a rel="dct:publisher"
+ href="https://github.com/ztombol">
+ <span property="dct:title">Zoltán Tömböl</span></a>
+ has waived all copyright and related or neighboring rights to
+ <span property="dct:title">bats-core</span>.
+This work is published from:
+<span property="vcard:Country" datatype="dct:ISO3166"
+ content="HU" about="https://github.com/ztombol">
+ Hungary</span>.
+</p>
+
+<!-- END GENERATED -->
+
+
+<!-- REFERENCES -->
+
+[bats]: https://github.com/sstephenson/bats
+[bats-docs]: https://github.com/ztombol/bats-docs
+[bats-assert]: https://github.com/ztombol/bats-assert
diff --git a/load.bash b/load.bash
new file mode 100644
index 0000000..325c5b1
--- /dev/null
+++ b/load.bash
@@ -0,0 +1 @@
+source "$(dirname "${BASH_SOURCE[0]}")/src/output.bash"
diff --git a/src/output.bash b/src/output.bash
new file mode 100644
index 0000000..e407a9a
--- /dev/null
+++ b/src/output.bash
@@ -0,0 +1,279 @@
+#
+# bats-core - Supporting library for Bats test helpers
+#
+# Written in 2016 by Zoltan Tombol <zoltan dot tombol at gmail dot com>
+#
+# To the extent possible under law, the author(s) have dedicated all
+# copyright and related and neighboring rights to this software to the
+# public domain worldwide. This software is distributed without any
+# warranty.
+#
+# You should have received a copy of the CC0 Public Domain Dedication
+# along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+#
+
+#
+# output.bash
+# -----------
+#
+# Private functions implementing output formatting. Used by public
+# helper functions.
+#
+
+# Print a message to the standard error. When no parameters are
+# specified, the message is read from the standard input.
+#
+# Globals:
+# none
+# Arguments:
+# $@ - [=STDIN] message
+# Returns:
+# none
+# Inputs:
+# STDIN - [=$@] message
+# Outputs:
+# STDERR - message
+batslib_err() {
+ { if (( $# > 0 )); then
+ echo "$@"
+ else
+ cat -
+ fi
+ } >&2
+}
+
+# Count the number of lines in the given string.
+#
+# TODO(ztombol): Fix tests and remove this note after #93 is resolved!
+# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not
+# give the same result as `${#lines[@]}' when the output contains
+# empty lines.
+# See PR #93 (https://github.com/sstephenson/bats/pull/93).
+#
+# Globals:
+# none
+# Arguments:
+# $1 - string
+# Returns:
+# none
+# Outputs:
+# STDOUT - number of lines
+batslib_count_lines() {
+ local -i n_lines=0
+ local line
+ while IFS='' read -r line || [[ -n $line ]]; do
+ (( ++n_lines ))
+ done < <(printf '%s' "$1")
+ echo "$n_lines"
+}
+
+# Determine whether all strings are single-line.
+#
+# Globals:
+# none
+# Arguments:
+# $@ - strings
+# Returns:
+# 0 - all strings are single-line
+# 1 - otherwise
+batslib_is_single_line() {
+ for string in "$@"; do
+ (( $(batslib_count_lines "$string") > 1 )) && return 1
+ done
+ return 0
+}
+
+# Determine the length of the longest key that has a single-line value.
+#
+# This function is useful in determining the correct width of the key
+# column in two-column format when some keys may have multi-line values
+# and thus should be excluded.
+#
+# Globals:
+# none
+# Arguments:
+# $odd - key
+# $even - value of the previous key
+# Returns:
+# none
+# Outputs:
+# STDOUT - length of longest key
+batslib_get_max_single_line_key_width() {
+ local -i max_len=-1
+ while (( $# != 0 )); do
+ local -i key_len="${#1}"
+ batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len"
+ shift 2
+ done
+ echo "$max_len"
+}
+
+# Print key-value pairs in two-column format.
+#
+# Keys are displayed in the first column, and their corresponding values
+# in the second. To evenly line up values, the key column is fixed-width
+# and its width is specified with the first parameter (possibly computed
+# using `batslib_get_max_single_line_key_width').
+#
+# Globals:
+# none
+# Arguments:
+# $1 - width of key column
+# $even - key
+# $odd - value of the previous key
+# Returns:
+# none
+# Outputs:
+# STDOUT - formatted key-value pairs
+batslib_print_kv_single() {
+ local -ir col_width="$1"; shift
+ while (( $# != 0 )); do
+ printf '%-*s : %s\n' "$col_width" "$1" "$2"
+ shift 2
+ done
+}
+
+# Print key-value pairs in multi-line format.
+#
+# The key is displayed first with the number of lines of its
+# corresponding value in parenthesis. Next, starting on the next line,
+# the value is displayed. For better readability, it is recommended to
+# indent values using `batslib_prefix'.
+#
+# Globals:
+# none
+# Arguments:
+# $odd - key
+# $even - value of the previous key
+# Returns:
+# none
+# Outputs:
+# STDOUT - formatted key-value pairs
+batslib_print_kv_multi() {
+ while (( $# != 0 )); do
+ printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )"
+ printf '%s\n' "$2"
+ shift 2
+ done
+}
+
+# Print all key-value pairs in either two-column or multi-line format
+# depending on whether all values are single-line.
+#
+# If all values are single-line, print all pairs in two-column format
+# with the specified key column width (identical to using
+# `batslib_print_kv_single').
+#
+# Otherwise, print all pairs in multi-line format after indenting values
+# with two spaces for readability (identical to using `batslib_prefix'
+# and `batslib_print_kv_multi')
+#
+# Globals:
+# none
+# Arguments:
+# $1 - width of key column (for two-column format)
+# $even - key
+# $odd - value of the previous key
+# Returns:
+# none
+# Outputs:
+# STDOUT - formatted key-value pairs
+batslib_print_kv_single_or_multi() {
+ local -ir width="$1"; shift
+ local -a pairs=( "$@" )
+
+ local -a values=()
+ local -i i
+ for (( i=1; i < ${#pairs[@]}; i+=2 )); do
+ values+=( "${pairs[$i]}" )
+ done
+
+ if batslib_is_single_line "${values[@]}"; then
+ batslib_print_kv_single "$width" "${pairs[@]}"
+ else
+ local -i i
+ for (( i=1; i < ${#pairs[@]}; i+=2 )); do
+ pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )"
+ done
+ batslib_print_kv_multi "${pairs[@]}"
+ fi
+}
+
+# Prefix each line read from the standard input with the given string.
+#
+# Globals:
+# none
+# Arguments:
+# $1 - [= ] prefix string
+# Returns:
+# none
+# Inputs:
+# STDIN - lines
+# Outputs:
+# STDOUT - prefixed lines
+batslib_prefix() {
+ local -r prefix="${1:- }"
+ local line
+ while IFS='' read -r line || [[ -n $line ]]; do
+ printf '%s%s\n' "$prefix" "$line"
+ done
+}
+
+# Mark select lines of the text read from the standard input by
+# overwriting their beginning with the given string.
+#
+# Usually the input is indented by a few spaces using `batslib_prefix'
+# first.
+#
+# Globals:
+# none
+# Arguments:
+# $1 - marking string
+# $@ - indices (zero-based) of lines to mark
+# Returns:
+# none
+# Inputs:
+# STDIN - lines
+# Outputs:
+# STDOUT - lines after marking
+batslib_mark() {
+ local -r symbol="$1"; shift
+ # Sort line numbers.
+ set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" )
+
+ local line
+ local -i idx=0
+ while IFS='' read -r line || [[ -n $line ]]; do
+ if (( ${1:--1} == idx )); then
+ printf '%s\n' "${symbol}${line:${#symbol}}"
+ shift
+ else
+ printf '%s\n' "$line"
+ fi
+ (( ++idx ))
+ done
+}
+
+# Enclose the input text in header and footer lines.
+#
+# The header contains the given string as title. The output is preceded
+# and followed by an additional newline to make it stand out more.
+#
+# Globals:
+# none
+# Arguments:
+# $1 - title
+# Returns:
+# none
+# Inputs:
+# STDIN - text
+# Outputs:
+# STDOUT - decorated text
+batslib_decorate() {
+ echo
+ echo "-- $1 --"
+ cat -
+ echo '--'
+ echo
+}
diff --git a/test/50-output-10-batslib_err.bats b/test/50-output-10-batslib_err.bats
new file mode 100755
index 0000000..8c27fd1
--- /dev/null
+++ b/test/50-output-10-batslib_err.bats
@@ -0,0 +1,16 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_err() <message...>: displays <message...>' {
+ run batslib_err 'm1' 'm2'
+ [ "$status" -eq 0 ]
+ [ "$output" == 'm1 m2' ]
+}
+
+@test 'batslib_err(): reads <message...> from STDIN' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ echo 'm1' 'm2' | batslib_err"
+ [ "$status" -eq 0 ]
+ [ "$output" == 'm1 m2' ]
+}
diff --git a/test/50-output-11-batslib_count_lines.bats b/test/50-output-11-batslib_count_lines.bats
new file mode 100755
index 0000000..ea172c3
--- /dev/null
+++ b/test/50-output-11-batslib_count_lines.bats
@@ -0,0 +1,21 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_count_lines() <string>: displays the number of lines in <string>' {
+ run batslib_count_lines $'a\nb\nc\n'
+ [ "$status" -eq 0 ]
+ [ "$output" == '3' ]
+}
+
+@test 'batslib_count_lines() <string>: counts the last line when it is not terminated by a newline' {
+ run batslib_count_lines $'a\nb\nc'
+ [ "$status" -eq 0 ]
+ [ "$output" == '3' ]
+}
+
+@test 'batslib_count_lines() <string>: counts empty lines' {
+ run batslib_count_lines $'\n\n\n'
+ [ "$status" -eq 0 ]
+ [ "$output" == '3' ]
+}
diff --git a/test/50-output-12-batslib_is_single_line.bats b/test/50-output-12-batslib_is_single_line.bats
new file mode 100755
index 0000000..484b64d
--- /dev/null
+++ b/test/50-output-12-batslib_is_single_line.bats
@@ -0,0 +1,13 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_is_single_line() <string...>: returns 0 if all <string...> are single-line' {
+ run batslib_is_single_line 'a' $'b\n' 'c'
+ [ "$status" -eq 0 ]
+}
+
+@test 'batslib_is_single_line() <string...>: returns 1 if at least one of <string...> is longer than one line' {
+ run batslib_is_single_line 'a' $'b\nb' 'c'
+ [ "$status" -eq 1 ]
+}
diff --git a/test/50-output-13-batslib_get_max_single_line_key_width.bats b/test/50-output-13-batslib_get_max_single_line_key_width.bats
new file mode 100755
index 0000000..e6af161
--- /dev/null
+++ b/test/50-output-13-batslib_get_max_single_line_key_width.bats
@@ -0,0 +1,21 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_get_max_single_line_key_width() <pair...>: displays the length of the longest key' {
+ local -ar pairs=( 'k _1' 'v 1'
+ 'k 2' 'v 2'
+ 'k __3' 'v 3' )
+ run batslib_get_max_single_line_key_width "${pairs[@]}"
+ [ "$status" -eq 0 ]
+ [ "$output" == '5' ]
+}
+
+@test 'batslib_get_max_single_line_key_width() <pair...>: only considers keys with single-line values' {
+ local -ar pairs=( 'k _1' 'v 1'
+ 'k 2' 'v 2'
+ 'k __3' $'v\n3' )
+ run batslib_get_max_single_line_key_width "${pairs[@]}"
+ [ "$status" -eq 0 ]
+ [ "$output" == '4' ]
+}
diff --git a/test/50-output-14-batslib_print_kv_single.bats b/test/50-output-14-batslib_print_kv_single.bats
new file mode 100755
index 0000000..7637897
--- /dev/null
+++ b/test/50-output-14-batslib_print_kv_single.bats
@@ -0,0 +1,27 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_print_kv_single() <width> <pair...>: displays <pair...> in two-column format with <width> wide key column' {
+ local -ar pairs=( 'k _1' 'v 1'
+ 'k 2 ' 'v 2'
+ 'k __3' 'v 3' )
+ run batslib_print_kv_single 5 "${pairs[@]}"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" == '3' ]
+ [ "${lines[0]}" == 'k _1 : v 1' ]
+ [ "${lines[1]}" == 'k 2 : v 2' ]
+ [ "${lines[2]}" == 'k __3 : v 3' ]
+}
+
+@test 'batslib_print_kv_single() <width> <pair...>: does not truncate keys when the column is too narrow' {
+ local -ar pairs=( 'k _1' 'v 1'
+ 'k 2' 'v 2'
+ 'k __3' 'v 3' )
+ run batslib_print_kv_single 0 "${pairs[@]}"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" == '3' ]
+ [ "${lines[0]}" == 'k _1 : v 1' ]
+ [ "${lines[1]}" == 'k 2 : v 2' ]
+ [ "${lines[2]}" == 'k __3 : v 3' ]
+}
diff --git a/test/50-output-15-batslib_print_kv_multi.bats b/test/50-output-15-batslib_print_kv_multi.bats
new file mode 100755
index 0000000..6ad4b3d
--- /dev/null
+++ b/test/50-output-15-batslib_print_kv_multi.bats
@@ -0,0 +1,19 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_print_kv_multi() <pair...>: displays <pair...> in multi-line format' {
+ local -ar pairs=( 'k _1' 'v 1'
+ 'k 2' $'v 2-1\nv 2-2'
+ 'k __3' 'v 3' )
+ run batslib_print_kv_multi "${pairs[@]}"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" == '7' ]
+ [ "${lines[0]}" == 'k _1 (1 lines):' ]
+ [ "${lines[1]}" == 'v 1' ]
+ [ "${lines[2]}" == 'k 2 (2 lines):' ]
+ [ "${lines[3]}" == 'v 2-1' ]
+ [ "${lines[4]}" == 'v 2-2' ]
+ [ "${lines[5]}" == 'k __3 (1 lines):' ]
+ [ "${lines[6]}" == 'v 3' ]
+}
diff --git a/test/50-output-16-batslib_print_kv_single_or_multi.bats b/test/50-output-16-batslib_print_kv_single_or_multi.bats
new file mode 100755
index 0000000..c20d101
--- /dev/null
+++ b/test/50-output-16-batslib_print_kv_single_or_multi.bats
@@ -0,0 +1,31 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_print_kv_single_or_multi() <width> <pair...>: displays <pair...> in two-column format if all values are one line long' {
+ local -ar pairs=( 'k _1' 'v 1'
+ 'k 2 ' 'v 2'
+ 'k __3' 'v 3' )
+ run batslib_print_kv_single_or_multi 5 "${pairs[@]}"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" == '3' ]
+ [ "${lines[0]}" == 'k _1 : v 1' ]
+ [ "${lines[1]}" == 'k 2 : v 2' ]
+ [ "${lines[2]}" == 'k __3 : v 3' ]
+}
+
+@test 'batslib_print_kv_single_or_multi() <width> <pair...>: displays <pair...> in multi-line format if at least one value is longer than one line' {
+ local -ar pairs=( 'k _1' 'v 1'
+ 'k 2' $'v 2-1\nv 2-2'
+ 'k __3' 'v 3' )
+ run batslib_print_kv_single_or_multi 5 "${pairs[@]}"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" == '7' ]
+ [ "${lines[0]}" == 'k _1 (1 lines):' ]
+ [ "${lines[1]}" == ' v 1' ]
+ [ "${lines[2]}" == 'k 2 (2 lines):' ]
+ [ "${lines[3]}" == ' v 2-1' ]
+ [ "${lines[4]}" == ' v 2-2' ]
+ [ "${lines[5]}" == 'k __3 (1 lines):' ]
+ [ "${lines[6]}" == ' v 3' ]
+}
diff --git a/test/50-output-17-batslib_prefix.bats b/test/50-output-17-batslib_prefix.bats
new file mode 100755
index 0000000..817fd33
--- /dev/null
+++ b/test/50-output-17-batslib_prefix.bats
@@ -0,0 +1,43 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_prefix() <prefix>: prefixes each line of the input with <prefix>' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf 'a\nb\nc\n' | batslib_prefix '_'"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '_a' ]
+ [ "${lines[1]}" == '_b' ]
+ [ "${lines[2]}" == '_c' ]
+}
+
+@test 'batslib_prefix() <prefix>: prefixes the last line when it is not terminated by a newline' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf 'a\nb\nc' | batslib_prefix '_'"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '_a' ]
+ [ "${lines[1]}" == '_b' ]
+ [ "${lines[2]}" == '_c' ]
+}
+
+@test 'batslib_prefix() <prefix>: prefixes empty lines' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf '\n\n\n' | batslib_prefix '_'"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '_' ]
+ [ "${lines[1]}" == '_' ]
+ [ "${lines[2]}" == '_' ]
+}
+
+@test 'batslib_prefix(): <prefix> default to two spaces' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf 'a\nb\nc\n' | batslib_prefix"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == ' a' ]
+ [ "${lines[1]}" == ' b' ]
+ [ "${lines[2]}" == ' c' ]
+}
diff --git a/test/50-output-18-batslib_mark.bats b/test/50-output-18-batslib_mark.bats
new file mode 100755
index 0000000..c5d0975
--- /dev/null
+++ b/test/50-output-18-batslib_mark.bats
@@ -0,0 +1,72 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_mark() <mark> <index>: marks the <index>-th line of the input with <mark>' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf ' a\n b\n c\n' | batslib_mark '>' 0"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '>a' ]
+ [ "${lines[1]}" == ' b' ]
+ [ "${lines[2]}" == ' c' ]
+}
+
+@test 'batslib_mark() <mark> <index...>: marks multiple lines when <index...> is in ascending order' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf ' a\n b\n c\n' | batslib_mark '>' 1 2"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == ' a' ]
+ [ "${lines[1]}" == '>b' ]
+ [ "${lines[2]}" == '>c' ]
+}
+
+@test 'batslib_mark() <mark> <index...>: marks multiple lines when <index...> is in random order' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf ' a\n b\n c\n d\n' | batslib_mark '>' 2 1 3"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == ' a' ]
+ [ "${lines[1]}" == '>b' ]
+ [ "${lines[2]}" == '>c' ]
+ [ "${lines[3]}" == '>d' ]
+}
+
+@test 'batslib_mark() <mark> <index...>: ignores duplicate indices' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf ' a\n b\n c\n' | batslib_mark '>' 1 2 1"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == ' a' ]
+ [ "${lines[1]}" == '>b' ]
+ [ "${lines[2]}" == '>c' ]
+}
+
+@test 'batslib_mark() <mark> <index...>: outputs the input untouched if <mark> is the empty string' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf ' a\n b\n c\n' | batslib_mark '' 1"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == ' a' ]
+ [ "${lines[1]}" == ' b' ]
+ [ "${lines[2]}" == ' c' ]
+}
+
+@test 'batslib_mark() <mark> <index>: marks the last line when it is not terminated by a newline' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf ' a\n b\n c' | batslib_mark '>' 2"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == ' a' ]
+ [ "${lines[1]}" == ' b' ]
+ [ "${lines[2]}" == '>c' ]
+}
+
+@test 'batslib_mark() <mark> <index>: does not truncate <mark> if it is longer than the marked line' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ printf '\n' | batslib_mark '>' 0"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 1 ]
+ [ "${lines[0]}" == '>' ]
+}
diff --git a/test/50-output-19-batslib_decorate.bats b/test/50-output-19-batslib_decorate.bats
new file mode 100755
index 0000000..02d55ad
--- /dev/null
+++ b/test/50-output-19-batslib_decorate.bats
@@ -0,0 +1,13 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_decorate() <title>: encloses the input in a footer line and a header line containing <title>' {
+ run bash -c "source '${TEST_MAIN_DIR}/load.bash'
+ echo 'body' | batslib_decorate 'title'"
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- title --' ]
+ [ "${lines[1]}" == 'body' ]
+ [ "${lines[2]}" == '--' ]
+}
diff --git a/test/test_helper.bash b/test/test_helper.bash
new file mode 100644
index 0000000..ca16775
--- /dev/null
+++ b/test/test_helper.bash
@@ -0,0 +1,6 @@
+setup() {
+ export TEST_MAIN_DIR="${BATS_TEST_DIRNAME}/.."
+
+ # Load library.
+ load '../load'
+}