diff --git a/06-automation/06-02-tools-bats/.ecrc b/06-automation/06-02-tools-bats/.ecrc
new file mode 100644
index 0000000..480b412
--- /dev/null
+++ b/06-automation/06-02-tools-bats/.ecrc
@@ -0,0 +1,18 @@
+{
+ "Verbose": true,
+ "Debug": false,
+ "IgnoreDefaults": false,
+ "SpacesAftertabs": false,
+ "NoColor": false,
+ "Exclude": ["src/test/docs_helper/.*", "src/main/bash/zsdoc/data/.*"],
+ "AllowedContentTypes": [],
+ "PassedFiles": ["src/", "dupa.yml"],
+ "Disable": {
+ "EndOfLine": false,
+ "Indentation": false,
+ "InsertFinalNewline": false,
+ "TrimTrailingWhitespace": false,
+ "IndentSize": false,
+ "MaxLineLength": false
+ }
+}
diff --git a/06-automation/06-02-tools-bats/.editorconfig b/06-automation/06-02-tools-bats/.editorconfig
new file mode 100644
index 0000000..aff0218
--- /dev/null
+++ b/06-automation/06-02-tools-bats/.editorconfig
@@ -0,0 +1,11 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+insert_final_newline = true
+
+[*.yml]
+indent_style = space
+indent_size = 2
diff --git a/06-automation/06-02-tools-bats/.gitignore b/06-automation/06-02-tools-bats/.gitignore
new file mode 100644
index 0000000..567609b
--- /dev/null
+++ b/06-automation/06-02-tools-bats/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/06-automation/06-02-tools-bats/Makefile b/06-automation/06-02-tools-bats/Makefile
new file mode 100644
index 0000000..60f2057
--- /dev/null
+++ b/06-automation/06-02-tools-bats/Makefile
@@ -0,0 +1,53 @@
+BUILD_DIR=build
+SHELLCHECK_VERSION=v0.7.1
+
+.PHONY: test
+test: shellcheck zsd bats editorconfigchecker
+
+
+# Zadanie uruchamiające editorconfig-checkera
+.PHONY: editorconfigchecker
+editorconfigchecker: install_editorconfigchecker_if_missing
+ $(BUILD_DIR)/editorconfigchecker/bin/ec
+
+# Zadanie instalujące editorconfigchecker, jesli nie został już zainstalowany / ściągnięty
+.PHONY: install_editorconfigchecker_if_missing
+install_editorconfigchecker_if_missing:
+ bash -c "(test -f $(BUILD_DIR)/editorconfigchecker/bin/ec || tools/build-helper.sh download-editorconfigchecker)"
+
+# Zadanie generujące dokumentację dla skryptów Bashowych.
+.PHONY: zsd
+zsd: install_zshelldoc_if_missing
+ tools/build-helper.sh generate-zsd
+
+# Zadanie instalujące ZShell Doc, jeśli nie został już zainstalowany / ściągnięty.
+.PHONY: install_zshelldoc_if_missing
+install_zshelldoc_if_missing:
+ bash -c "(which zsd || test -f $(BUILD_DIR)/zsd/bin/zsd && echo 'zsd zainstalowany') || tools/build-helper.sh install-zsd"
+
+# Zadanie uruchamiające Shellcheck.
+.PHONY: shellcheck
+shellcheck: install_shellcheck_if_missing
+ $(eval SHELLCHECK_CMD = $(shell bash -c "which shellcheck || echo '$(BUILD_DIR)/shellcheck-$(SHELLCHECK_VERSION)/shellcheck'"))
+ find src -name "*.sh" | grep -v /zsdoc/ | xargs $(SHELLCHECK_CMD)
+
+# Zadanie instalujące Shellcheck jeśli go nie ma.
+.PHONY: install_shellcheck_if_missing
+install_shellcheck_if_missing:
+ bash -c "(which shellcheck || test -f $(BUILD_DIR)/shellcheck-$(SHELLCHECK_VERSION)}/shellcheck && echo 'shellcheck zainstalowany') || tools/build-helper.sh download-shellcheck"
+
+# Zadanie uruchamiające testy Bats.
+.PHONY: bats
+bats: install_bats_if_missing
+ $(eval BATS_CMD = $(shell bash -c "which bats || echo '$(BUILD_DIR)/bats/bin/bats'"))
+ $(BATS_CMD) -t src/test/bats
+
+# Zadanie instalujące Bats, jeśli nie został już zainstalowany / ściągnięty.
+.PHONY: install_bats_if_missing
+install_bats_if_missing: submodules-init
+ bash -c "(which bats || test -f $(BUILD_DIR)/bats/bin/bats && echo 'bats zainstalowany') || tools/build-helper.sh download-bats"
+
+# Zadanie inicjujące submoduły gitowe.
+.PHONY: submodules-init
+submodules-init:
+ tools/build-helper.sh initialize-submodules
diff --git a/06-automation/06-02-tools-bats/README.adoc b/06-automation/06-02-tools-bats/README.adoc
new file mode 100644
index 0000000..dc9c472
--- /dev/null
+++ b/06-automation/06-02-tools-bats/README.adoc
@@ -0,0 +1,17 @@
+= Testowanie Skryptów z Frameworkiem Bats [06-02]
+
+Narzędzia do testowania i analizy statycznej skryptów są opakowane w Makefile, który poza ich uruchamianiem posiada też polecenia do ich zaciągnięcia. Czyli mozna po prostu uruchomić komendy:
+```bash
+make test
+```
+by wszystko skonfigurować, a następnie testy Bats / Shellcheck / Editorconfig.
+
+== Kod
+
+W pliku `tools/build-helper.sh` mamy skrypty Bashowe pomagające przy buildzie. Skrypt potrafi np. dociągnąć różne aplikacje.
+
+W katalogu `src/main/bash` mamy skrypty Bashowe, które chcemy przetestować.
+
+W katalogu `src/test/bats` mamy skrypty Bats do testów skryptów bashowych.
+
+W katalogu `src/main/bash/zsdoc` mamy dokumentację w Asciidoctor skryptów Bashowych (przez projekt https://github.com/zdharma/zshelldoc)
diff --git a/06-automation/06-02-tools-bats/src/main/bash/script.sh b/06-automation/06-02-tools-bats/src/main/bash/script.sh
new file mode 100755
index 0000000..6d007cb
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/script.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# Exit immediately if a simple command exits with a non-zero status
+set -o errexit
+# When errtrace is enabled, the ERR trap is also triggered when the error (a command returning a nonzero code) occurs inside a function or a subshell.
+set -o errtrace
+# If set, the return value of a pipeline is the value of the last (rightmost) command to
+# exit with a non-zero status, or zero if all commands in the pipeline exit successfully.
+# By default, pipelines only return a failure if the last command errors.
+# When used in combination with set -e, pipefail will make a script exit if any command
+# in a pipeline errors.
+set -o pipefail
+
+# synopsis {{{
+# Receives a request from an external system.
+# }}}
+
+export CURL_BIN="${CURL_BIN:-curl}"
+export URL="${URL:-https://reqres.in/api/users/2}"
+
+# FUNCTION: request {{{
+# Will call an external URL to retrieve a user.
+function request() {
+ "${CURL_BIN}" "${URL}" || echo "Failed to retrieve the request"
+} # }}}
+
+request
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/bodies/script.sh b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/bodies/script.sh
new file mode 100644
index 0000000..2dcd5a1
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/bodies/script.sh
@@ -0,0 +1,9 @@
+
+set -o errexit
+set -o errtrace
+set -o pipefail
+
+export CURL_BIN="${CURL_BIN:-curl}"
+export URL="${URL:-https://reqres.in/api/users/2}"
+
+request
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/bodies/script.sh.comments b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/bodies/script.sh.comments
new file mode 100644
index 0000000..c350cdb
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/bodies/script.sh.comments
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Exit immediately if a simple command exits with a non-zero status
+# When errtrace is enabled, the ERR trap is also triggered when the error (a command returning a nonzero code) occurs inside a function or a subshell.
+# If set, the return value of a pipeline is the value of the last (rightmost) command to
+# exit with a non-zero status, or zero if all commands in the pipeline exit successfully.
+# By default, pipelines only return a failure if the last command errors.
+# When used in combination with set -e, pipefail will make a script exit if any command
+# in a pipeline errors.
+
+# synopsis {{{
+# Receives a request from an external system.
+# }}}
+
+# FUNCTION: request {{{
+# Will call an external URL to retrieve a user. # }}}
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/call_tree.zsd b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/call_tree.zsd
new file mode 100644
index 0000000..0be3f0d
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/call_tree.zsd
@@ -0,0 +1 @@
+script.sh/zsd_script_body: script.sh/request
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/descriptions/script.sh/request b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/descriptions/script.sh/request
new file mode 100644
index 0000000..d100d89
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/descriptions/script.sh/request
@@ -0,0 +1 @@
+# Will call an external URL to retrieve a user.
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/request/script.sh/Script_Body_/CURL_BIN b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/request/script.sh/Script_Body_/CURL_BIN
new file mode 100644
index 0000000..e69de29
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/request/script.sh/Script_Body_/URL b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/request/script.sh/Script_Body_/URL
new file mode 100644
index 0000000..e69de29
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/zsd_script_body/script.sh/Script_Body_/CURL_BIN b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/zsd_script_body/script.sh/Script_Body_/CURL_BIN
new file mode 100644
index 0000000..e69de29
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/zsd_script_body/script.sh/Script_Body_/URL b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/env-use/script.sh/zsd_script_body/script.sh/Script_Body_/URL
new file mode 100644
index 0000000..e69de29
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/exports/script.sh/Script_Body_/CURL_BIN b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/exports/script.sh/Script_Body_/CURL_BIN
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/exports/script.sh/Script_Body_/CURL_BIN
@@ -0,0 +1 @@
+
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/exports/script.sh/Script_Body_/URL b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/exports/script.sh/Script_Body_/URL
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/exports/script.sh/Script_Body_/URL
@@ -0,0 +1 @@
+
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/extended/script.sh b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/extended/script.sh
new file mode 100644
index 0000000..6d007cb
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/extended/script.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# Exit immediately if a simple command exits with a non-zero status
+set -o errexit
+# When errtrace is enabled, the ERR trap is also triggered when the error (a command returning a nonzero code) occurs inside a function or a subshell.
+set -o errtrace
+# If set, the return value of a pipeline is the value of the last (rightmost) command to
+# exit with a non-zero status, or zero if all commands in the pipeline exit successfully.
+# By default, pipelines only return a failure if the last command errors.
+# When used in combination with set -e, pipefail will make a script exit if any command
+# in a pipeline errors.
+set -o pipefail
+
+# synopsis {{{
+# Receives a request from an external system.
+# }}}
+
+export CURL_BIN="${CURL_BIN:-curl}"
+export URL="${URL:-https://reqres.in/api/users/2}"
+
+# FUNCTION: request {{{
+# Will call an external URL to retrieve a user.
+function request() {
+ "${CURL_BIN}" "${URL}" || echo "Failed to retrieve the request"
+} # }}}
+
+request
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/features/script.sh/Script_Body_/export b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/features/script.sh/Script_Body_/export
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/features/script.sh/Script_Body_/export
@@ -0,0 +1 @@
+
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/functions/script.sh/request b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/functions/script.sh/request
new file mode 100644
index 0000000..2896120
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/functions/script.sh/request
@@ -0,0 +1 @@
+"${CURL_BIN}" "${URL}" || echo "Failed to retrieve the request"
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/rev_call_tree.zsd b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/rev_call_tree.zsd
new file mode 100644
index 0000000..5882f08
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/rev_call_tree.zsd
@@ -0,0 +1 @@
+script.sh/request: script.sh/zsd_script_body
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/trees/script.sh/Script_Body_.tree b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/trees/script.sh/Script_Body_.tree
new file mode 100644
index 0000000..ad1b912
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/data/trees/script.sh/Script_Body_.tree
@@ -0,0 +1,4 @@
+Script_Body_
+`-- request
+
+1 directory, 0 files
diff --git a/06-automation/06-02-tools-bats/src/main/bash/zsdoc/script.sh.adoc b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/script.sh.adoc
new file mode 100644
index 0000000..bfc6e12
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/main/bash/zsdoc/script.sh.adoc
@@ -0,0 +1,49 @@
+script.sh(1)
+============
+:compat-mode!:
+
+NAME
+----
+script.sh - a shell script
+
+SYNOPSIS
+--------
+
+Receives a request from an external system.
+
+
+FUNCTIONS
+---------
+
+ request
+
+DETAILS
+-------
+
+Script Body
+~~~~~~~~~~~
+
+Has 9 line(s). Calls functions:
+
+ Script-Body
+ `-- request
+
+Uses feature(s): _export_
+
+_Exports (environment):_ CURL_BIN [big]*//* URL
+
+request
+~~~~~~~
+
+____
+ # Will call an external URL to retrieve a user.
+____
+
+Has 1 line(s). Doesn't call other functions.
+
+Called by:
+
+ Script-Body
+
+_Environment variables used:_ CURL_BIN [big]*//* URL
+
diff --git a/06-automation/06-02-tools-bats/src/test/bats/script.bats b/06-automation/06-02-tools-bats/src/test/bats/script.bats
new file mode 100644
index 0000000..3ac695a
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/script.bats
@@ -0,0 +1,51 @@
+#!/usr/bin/env bats
+
+load 'test_helper'
+load 'test_helper/bats-support/load'
+load 'test_helper/bats-assert/load'
+
+setup() {
+ export TEMP_DIR="$( mktemp -d )"
+ cp -a "${SOURCE_DIR}" "${TEMP_DIR}/my-test"
+}
+
+function curl_verbose { echo "curl $*"; }
+function curl_stub { echo "hello"; }
+function curl_exception { return 1; }
+
+export -f curl_verbose
+export -f curl_stub
+export -f curl_exception
+
+teardown() { rm -rf "${TEMP_DIR}"; }
+
+# Whitebox testing
+@test "should curl request to an external website" {
+ export CURL_BIN="curl_verbose"
+ export URL="https://foo.com/bar"
+
+ run "${SOURCE_DIR}/script.sh"
+
+ assert_success
+ assert_output "curl https://foo.com/bar"
+}
+
+# Blackbox testing
+@test "should return a response from an external website" {
+ export CURL_BIN="curl_stub"
+
+ run "${SOURCE_DIR}/script.sh"
+
+ assert_success
+ assert_output "hello"
+}
+
+# Failure testing
+@test "should not fail when request retrieval failed" {
+ export CURL_BIN="curl_exception"
+
+ run "${SOURCE_DIR}/script.sh"
+
+ assert_success
+ assert_output --partial "Failed"
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper.bash
new file mode 100644
index 0000000..29266d7
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper.bash
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures"
+SOURCE_DIR="${BATS_TEST_DIRNAME}/../../main/bash"
+
+export FIXTURES_DIR SOURCE_DIR
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/.travis.yml b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/.travis.yml
new file mode 100644
index 0000000..e56169a
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/.travis.yml
@@ -0,0 +1,8 @@
+language: bash
+before_install:
+ - ./script/install-bats.sh
+ - git clone --depth 1 https://github.com/ztombol/bats-support ../bats-support
+before_script:
+ - export PATH="${HOME}/.local/bin:${PATH}"
+script:
+ - bats test
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/CHANGELOG.md b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/CHANGELOG.md
new file mode 100644
index 0000000..7e326f4
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/CHANGELOG.md
@@ -0,0 +1,39 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+This project adheres to [Semantic Versioning](http://semver.org/).
+
+
+## [0.3.0] - 2016-03-22
+
+### Removed
+
+- Move `fail()` to `bats-support`
+
+
+## [0.2.0] - 2016-03-11
+
+### Added
+
+- `refute()` to complement `assert()`
+- `npm` support
+
+### Fixed
+
+- Not consuming the `--` when stopping option parsing in
+ `assert_output`, `refute_output`, `assert_line` and `refute_line`
+
+
+## 0.1.0 - 2016-02-16
+
+### Added
+
+- Reporting arbitrary failures with `fail()`
+- Generic assertions with `assert()` and `assert_equal()`
+- Testing exit status with `assert_success()` and `assert_failure()`
+- Testing output with `assert_output()` and `refute_output()`
+- Testing individual lines with `assert_line()` and `refute_line()`
+
+
+[0.3.0]: https://github.com/ztombol/bats-assert/compare/v0.2.0...v0.3.0
+[0.2.0]: https://github.com/ztombol/bats-assert/compare/v0.1.0...v0.2.0
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/LICENSE b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/LICENSE
new file mode 100644
index 0000000..670154e
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/LICENSE
@@ -0,0 +1,116 @@
+CC0 1.0 Universal
+
+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.
+
+For more information, please see
+
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/README.md b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/README.md
new file mode 100644
index 0000000..2e60e92
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/README.md
@@ -0,0 +1,656 @@
+# bats-assert
+
+[](https://raw.githubusercontent.com/ztombol/bats-assert/master/LICENSE)
+[](https://github.com/ztombol/bats-assert/releases/latest)
+[](https://travis-ci.org/ztombol/bats-assert)
+
+`bats-assert` is a helper library providing common assertions for
+[Bats][bats].
+
+Assertions are functions that perform a test and output relevant
+information on failure to help debugging. They return 1 on failure and 0
+otherwise. Output, [formatted][bats-support-output] for readability, is
+sent to the standard error to make assertions usable outside of `@test`
+blocks too.
+
+Assertions testing exit code and output operate on the results of the
+most recent invocation of `run`.
+
+Dependencies:
+- [`bats-support`][bats-support] (formerly `bats-core`) - output
+ formatting
+
+See the [shared documentation][bats-docs] to learn how to install and
+load this library.
+
+
+## Usage
+
+### `assert`
+
+Fail if the given expression evaluates to false.
+
+***Note:*** *The expression must be a simple command. [Compound
+commands][bash-comp-cmd], such as `[[`, can be used only when executed
+with `bash -c`.*
+
+```bash
+@test 'assert()' {
+ touch '/var/log/test.log'
+ assert [ -e '/var/log/test.log' ]
+}
+```
+
+On failure, the failed expression is displayed.
+
+```
+-- assertion failed --
+expression : [ -e /var/log/test.log ]
+--
+```
+
+
+### `refute`
+
+Fail if the given expression evaluates to true.
+
+***Note:*** *The expression must be a simple command. [Compound
+commands][bash-comp-cmd], such as `[[`, can be used only when executed
+with `bash -c`.*
+
+```bash
+@test 'refute()' {
+ rm -f '/var/log/test.log'
+ refute [ -e '/var/log/test.log' ]
+}
+```
+
+On failure, the successful expression is displayed.
+
+```
+-- assertion succeeded, but it was expected to fail --
+expression : [ -e /var/log/test.log ]
+--
+```
+
+
+### `assert_equal`
+
+Fail if the two parameters, actual and expected value respectively, do
+not equal.
+
+```bash
+@test 'assert_equal()' {
+ assert_equal 'have' 'want'
+}
+```
+
+On failure, the expected and actual values are displayed.
+
+```
+-- values do not equal --
+expected : want
+actual : have
+--
+```
+
+If either value is longer than one line both are displayed in
+*multi-line* format.
+
+
+### `assert_success`
+
+Fail if `$status` is not 0.
+
+```bash
+@test 'assert_success() status only' {
+ run bash -c "echo 'Error!'; exit 1"
+ assert_success
+}
+```
+
+On failure, `$status` and `$output` are displayed.
+
+```
+-- command failed --
+status : 1
+output : Error!
+--
+```
+
+If `$output` is longer than one line, it is displayed in *multi-line*
+format.
+
+
+### `assert_failure`
+
+Fail if `$status` is 0.
+
+```bash
+@test 'assert_failure() status only' {
+ run echo 'Success!'
+ assert_failure
+}
+```
+
+On failure, `$output` is displayed.
+
+```
+-- command succeeded, but it was expected to fail --
+output : Success!
+--
+```
+
+If `$output` is longer than one line, it is displayed in *multi-line*
+format.
+
+#### Expected status
+
+When one parameter is specified, fail if `$status` does not equal the
+expected status specified by the parameter.
+
+```bash
+@test 'assert_failure() with expected status' {
+ run bash -c "echo 'Error!'; exit 1"
+ assert_failure 2
+}
+```
+
+On failure, the expected and actual status, and `$output` are displayed.
+
+```
+-- command failed as expected, but status differs --
+expected : 2
+actual : 1
+output : Error!
+--
+```
+
+If `$output` is longer than one line, it is displayed in *multi-line*
+format.
+
+
+### `assert_output`
+
+This function helps to verify that a command or function produces the
+correct output by checking that the specified expected output matches
+the actual output. Matching can be literal (default), partial or regular
+expression. This function is the logical complement of `refute_output`.
+
+#### Literal matching
+
+By default, literal matching is performed. The assertion fails if
+`$output` does not equal the expected output.
+
+```bash
+@test 'assert_output()' {
+ run echo 'have'
+ assert_output 'want'
+}
+```
+
+The expected output can be specified with a heredoc or standard input as well.
+
+```bash
+@test 'assert_output() with pipe' {
+ run echo 'have'
+ echo 'want' | assert_output
+}
+```
+
+On failure, the expected and actual output are displayed.
+
+```
+-- output differs --
+expected : want
+actual : have
+--
+```
+
+If either value is longer than one line both are displayed in
+*multi-line* format.
+
+#### Partial matching
+
+Partial matching can be enabled with the `--partial` option (`-p` for
+short). When used, the assertion fails if the expected *substring* is
+not found in `$output`.
+
+```bash
+@test 'assert_output() partial matching' {
+ run echo 'ERROR: no such file or directory'
+ assert_output --partial 'SUCCESS'
+}
+```
+
+On failure, the substring and the output are displayed.
+
+```
+-- output does not contain substring --
+substring : SUCCESS
+output : ERROR: no such file or directory
+--
+```
+
+This option and regular expression matching (`--regexp` or `-e`) are
+mutually exclusive. An error is displayed when used simultaneously.
+
+#### Regular expression matching
+
+Regular expression matching can be enabled with the `--regexp` option
+(`-e` for short). When used, the assertion fails if the *extended
+regular expression* does not match `$output`.
+
+*Note: The anchors `^` and `$` bind to the beginning and the end of the
+entire output (not individual lines), respectively.*
+
+```bash
+@test 'assert_output() regular expression matching' {
+ run echo 'Foobar 0.1.0'
+ assert_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
+}
+```
+
+On failure, the regular expression and the output are displayed.
+
+```
+-- regular expression does not match output --
+regexp : ^Foobar v[0-9]+\.[0-9]+\.[0-9]$
+output : Foobar 0.1.0
+--
+```
+
+An error is displayed if the specified extended regular expression is
+invalid.
+
+This option and partial matching (`--partial` or `-p`) are mutually
+exclusive. An error is displayed when used simultaneously.
+
+
+### `refute_output`
+
+This function helps to verify that a command or function produces the
+correct output by checking that the specified unexpected output does not
+match the actual output. Matching can be literal (default), partial or
+regular expression. This function is the logical complement of
+`assert_output`.
+
+#### Literal matching
+
+By default, literal matching is performed. The assertion fails if
+`$output` equals the unexpected output.
+
+```bash
+@test 'refute_output()' {
+ run echo 'want'
+ refute_output 'want'
+}
+```
+
+-The unexpected output can be specified with a heredoc or standard input as well.
+
+```bash
+@test 'refute_output() with pipe' {
+ run echo 'want'
+ echo 'want' | refute_output
+}
+```
+
+On failure, the output is displayed.
+
+```
+-- output equals, but it was expected to differ --
+output : want
+--
+```
+
+If output is longer than one line it is displayed in *multi-line*
+format.
+
+#### Partial matching
+
+Partial matching can be enabled with the `--partial` option (`-p` for
+short). When used, the assertion fails if the unexpected *substring* is
+found in `$output`.
+
+```bash
+@test 'refute_output() partial matching' {
+ run echo 'ERROR: no such file or directory'
+ refute_output --partial 'ERROR'
+}
+```
+
+On failure, the substring and the output are displayed.
+
+```
+-- output should not contain substring --
+substring : ERROR
+output : ERROR: no such file or directory
+--
+```
+
+This option and regular expression matching (`--regexp` or `-e`) are
+mutually exclusive. An error is displayed when used simultaneously.
+
+#### Regular expression matching
+
+Regular expression matching can be enabled with the `--regexp` option
+(`-e` for short). When used, the assertion fails if the *extended
+regular expression* matches `$output`.
+
+*Note: The anchors `^` and `$` bind to the beginning and the end of the
+entire output (not individual lines), respectively.*
+
+```bash
+@test 'refute_output() regular expression matching' {
+ run echo 'Foobar v0.1.0'
+ refute_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
+}
+```
+
+On failure, the regular expression and the output are displayed.
+
+```
+-- regular expression should not match output --
+regexp : ^Foobar v[0-9]+\.[0-9]+\.[0-9]$
+output : Foobar v0.1.0
+--
+```
+
+An error is displayed if the specified extended regular expression is
+invalid.
+
+This option and partial matching (`--partial` or `-p`) are mutually
+exclusive. An error is displayed when used simultaneously.
+
+
+### `assert_line`
+
+Similarly to `assert_output`, this function helps to verify that a
+command or function produces the correct output. It checks that the
+expected line appears in the output (default) or in a specific line of
+it. Matching can be literal (default), partial or regular expression.
+This function is the logical complement of `refute_line`.
+
+***Warning:*** *Due to a [bug in Bats][bats-93], empty lines are
+discarded from `${lines[@]}`, causing line indices to change and
+preventing testing for empty lines.*
+
+[bats-93]: https://github.com/sstephenson/bats/pull/93
+
+#### Looking for a line in the output
+
+By default, the entire output is searched for the expected line. The
+assertion fails if the expected line is not found in `${lines[@]}`.
+
+```bash
+@test 'assert_line() looking for line' {
+ run echo $'have-0\nhave-1\nhave-2'
+ assert_line 'want'
+}
+```
+
+On failure, the expected line and the output are displayed.
+
+***Warning:*** *The output displayed does not contain empty lines. See
+the Warning above for more.*
+
+```
+-- output does not contain line --
+line : want
+output (3 lines):
+ have-0
+ have-1
+ have-2
+--
+```
+
+If output is not longer than one line, it is displayed in *two-column*
+format.
+
+#### Matching a specific line
+
+When the `--index ` option is used (`-n ` for short) , the
+expected line is matched only against the line identified by the given
+index. The assertion fails if the expected line does not equal
+`${lines[]}`.
+
+```bash
+@test 'assert_line() specific line' {
+ run echo $'have-0\nhave-1\nhave-2'
+ assert_line --index 1 'want-1'
+}
+```
+
+On failure, the index and the compared lines are displayed.
+
+```
+-- line differs --
+index : 1
+expected : want-1
+actual : have-1
+--
+```
+
+#### Partial matching
+
+Partial matching can be enabled with the `--partial` option (`-p` for
+short). When used, a match fails if the expected *substring* is not
+found in the matched line.
+
+```bash
+@test 'assert_line() partial matching' {
+ run echo $'have 1\nhave 2\nhave 3'
+ assert_line --partial 'want'
+}
+```
+
+On failure, the same details are displayed as for literal matching,
+except that the substring replaces the expected line.
+
+```
+-- no output line contains substring --
+substring : want
+output (3 lines):
+ have 1
+ have 2
+ have 3
+--
+```
+
+This option and regular expression matching (`--regexp` or `-e`) are
+mutually exclusive. An error is displayed when used simultaneously.
+
+#### Regular expression matching
+
+Regular expression matching can be enabled with the `--regexp` option
+(`-e` for short). When used, a match fails if the *extended regular
+expression* does not match the line being tested.
+
+*Note: As expected, the anchors `^` and `$` bind to the beginning and
+the end of the matched line, respectively.*
+
+```bash
+@test 'assert_line() regular expression matching' {
+ run echo $'have-0\nhave-1\nhave-2'
+ assert_line --index 1 --regexp '^want-[0-9]$'
+}
+```
+
+On failure, the same details are displayed as for literal matching,
+except that the regular expression replaces the expected line.
+
+```
+-- regular expression does not match line --
+index : 1
+regexp : ^want-[0-9]$
+line : have-1
+--
+```
+
+An error is displayed if the specified extended regular expression is
+invalid.
+
+This option and partial matching (`--partial` or `-p`) are mutually
+exclusive. An error is displayed when used simultaneously.
+
+
+### `refute_line`
+
+Similarly to `refute_output`, this function helps to verify that a
+command or function produces the correct output. It checks that the
+unexpected line does not appear in the output (default) or in a specific
+line of it. Matching can be literal (default), partial or regular
+expression. This function is the logical complement of `assert_line`.
+
+***Warning:*** *Due to a [bug in Bats][bats-93], empty lines are
+discarded from `${lines[@]}`, causing line indices to change and
+preventing testing for empty lines.*
+
+[bats-93]: https://github.com/sstephenson/bats/pull/93
+
+#### Looking for a line in the output
+
+By default, the entire output is searched for the unexpected line. The
+assertion fails if the unexpected line is found in `${lines[@]}`.
+
+```bash
+@test 'refute_line() looking for line' {
+ run echo $'have-0\nwant\nhave-2'
+ refute_line 'want'
+}
+```
+
+On failure, the unexpected line, the index of its first match and the
+output with the matching line highlighted are displayed.
+
+***Warning:*** *The output displayed does not contain empty lines. See
+the Warning above for more.*
+
+```
+-- line should not be in output --
+line : want
+index : 1
+output (3 lines):
+ have-0
+> want
+ have-2
+--
+```
+
+If output is not longer than one line, it is displayed in *two-column*
+format.
+
+#### Matching a specific line
+
+When the `--index ` option is used (`-n ` for short) , the
+unexpected line is matched only against the line identified by the given
+index. The assertion fails if the unexpected line equals
+`${lines[]}`.
+
+```bash
+@test 'refute_line() specific line' {
+ run echo $'have-0\nwant-1\nhave-2'
+ refute_line --index 1 'want-1'
+}
+```
+
+On failure, the index and the unexpected line are displayed.
+
+```
+-- line should differ --
+index : 1
+line : want-1
+--
+```
+
+#### Partial matching
+
+Partial matching can be enabled with the `--partial` option (`-p` for
+short). When used, a match fails if the unexpected *substring* is found
+in the matched line.
+
+```bash
+@test 'refute_line() partial matching' {
+ run echo $'have 1\nwant 2\nhave 3'
+ refute_line --partial 'want'
+}
+```
+
+On failure, in addition to the details of literal matching, the
+substring is also displayed. When used with `--index ` the
+substring replaces the unexpected line.
+
+```
+-- no line should contain substring --
+substring : want
+index : 1
+output (3 lines):
+ have 1
+> want 2
+ have 3
+--
+```
+
+This option and regular expression matching (`--regexp` or `-e`) are
+mutually exclusive. An error is displayed when used simultaneously.
+
+#### Regular expression matching
+
+Regular expression matching can be enabled with the `--regexp` option
+(`-e` for short). When used, a match fails if the *extended regular
+expression* matches the line being tested.
+
+*Note: As expected, the anchors `^` and `$` bind to the beginning and
+the end of the matched line, respectively.*
+
+```bash
+@test 'refute_line() regular expression matching' {
+ run echo $'Foobar v0.1.0\nRelease date: 2015-11-29'
+ refute_line --index 0 --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
+}
+```
+
+On failure, in addition to the details of literal matching, the regular
+expression is also displayed. When used with `--index ` the regular
+expression replaces the unexpected line.
+
+```
+-- regular expression should not match line --
+index : 0
+regexp : ^Foobar v[0-9]+\.[0-9]+\.[0-9]$
+line : Foobar v0.1.0
+--
+```
+
+An error is displayed if the specified extended regular expression is
+invalid.
+
+This option and partial matching (`--partial` or `-p`) are mutually
+exclusive. An error is displayed when used simultaneously.
+
+
+## Options
+
+For functions that have options, `--` disables option parsing for the
+remaining arguments to allow using arguments identical to one of the
+allowed options.
+
+```bash
+assert_output -- '-p'
+```
+
+Specifying `--` as an argument is similarly simple.
+
+```bash
+refute_line -- '--'
+```
+
+
+
+
+[bats]: https://github.com/sstephenson/bats
+[bats-support-output]: https://github.com/ztombol/bats-support#output-formatting
+[bats-support]: https://github.com/ztombol/bats-support
+[bats-docs]: https://github.com/ztombol/bats-docs
+[bash-comp-cmd]: https://www.gnu.org/software/bash/manual/bash.html#Compound-Commands
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/load.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/load.bash
new file mode 100644
index 0000000..ac4a875
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/load.bash
@@ -0,0 +1 @@
+source "$(dirname "${BASH_SOURCE[0]}")/src/assert.bash"
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/package.json b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/package.json
new file mode 100644
index 0000000..d81b21c
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "bats-assert",
+ "version": "0.3.0",
+ "private": true,
+ "peerDependencies": {
+ "bats-support": "git+https://github.com/ztombol/bats-support.git#v0.2.0"
+ }
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/script/install-bats.sh b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/script/install-bats.sh
new file mode 100755
index 0000000..4c3161a
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/script/install-bats.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -o errexit
+set -o xtrace
+
+git clone --depth 1 https://github.com/sstephenson/bats
+cd bats && ./install.sh "${HOME}/.local" && cd .. && rm -rf bats
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/src/assert.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/src/assert.bash
new file mode 100644
index 0000000..1194753
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/src/assert.bash
@@ -0,0 +1,720 @@
+#
+# bats-assert - Common assertions for Bats
+#
+# Written in 2016 by Zoltan Tombol
+#
+# 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
+# .
+#
+
+#
+# assert.bash
+# -----------
+#
+# Assertions are functions that perform a test and output relevant
+# information on failure to help debugging. They return 1 on failure
+# and 0 otherwise.
+#
+# All output is formatted for readability using the functions of
+# `output.bash' and sent to the standard error.
+#
+
+# Fail and display the expression if it evaluates to false.
+#
+# NOTE: The expression must be a simple command. Compound commands, such
+# as `[[', can be used only when executed with `bash -c'.
+#
+# Globals:
+# none
+# Arguments:
+# $1 - expression
+# Returns:
+# 0 - expression evaluates to TRUE
+# 1 - otherwise
+# Outputs:
+# STDERR - details, on failure
+assert() {
+ if ! "$@"; then
+ batslib_print_kv_single 10 'expression' "$*" \
+ | batslib_decorate 'assertion failed' \
+ | fail
+ fi
+}
+
+# Fail and display the expression if it evaluates to true.
+#
+# NOTE: The expression must be a simple command. Compound commands, such
+# as `[[', can be used only when executed with `bash -c'.
+#
+# Globals:
+# none
+# Arguments:
+# $1 - expression
+# Returns:
+# 0 - expression evaluates to FALSE
+# 1 - otherwise
+# Outputs:
+# STDERR - details, on failure
+refute() {
+ if "$@"; then
+ batslib_print_kv_single 10 'expression' "$*" \
+ | batslib_decorate 'assertion succeeded, but it was expected to fail' \
+ | fail
+ fi
+}
+
+# Fail and display details if the expected and actual values do not
+# equal. Details include both values.
+#
+# Globals:
+# none
+# Arguments:
+# $1 - actual value
+# $2 - expected value
+# Returns:
+# 0 - values equal
+# 1 - otherwise
+# Outputs:
+# STDERR - details, on failure
+assert_equal() {
+ if [[ $1 != "$2" ]]; then
+ batslib_print_kv_single_or_multi 8 \
+ 'expected' "$2" \
+ 'actual' "$1" \
+ | batslib_decorate 'values do not equal' \
+ | fail
+ fi
+}
+
+# Fail and display details if `$status' is not 0. Details include
+# `$status' and `$output'.
+#
+# Globals:
+# status
+# output
+# Arguments:
+# none
+# Returns:
+# 0 - `$status' is 0
+# 1 - otherwise
+# Outputs:
+# STDERR - details, on failure
+assert_success() {
+ if (( status != 0 )); then
+ { local -ir width=6
+ batslib_print_kv_single "$width" 'status' "$status"
+ batslib_print_kv_single_or_multi "$width" 'output' "$output"
+ } | batslib_decorate 'command failed' \
+ | fail
+ fi
+}
+
+# Fail and display details if `$status' is 0. Details include `$output'.
+#
+# Optionally, when the expected status is specified, fail when it does
+# not equal `$status'. In this case, details include the expected and
+# actual status, and `$output'.
+#
+# Globals:
+# status
+# output
+# Arguments:
+# $1 - [opt] expected status
+# Returns:
+# 0 - `$status' is not 0, or
+# `$status' equals the expected status
+# 1 - otherwise
+# Outputs:
+# STDERR - details, on failure
+assert_failure() {
+ (( $# > 0 )) && local -r expected="$1"
+ if (( status == 0 )); then
+ batslib_print_kv_single_or_multi 6 'output' "$output" \
+ | batslib_decorate 'command succeeded, but it was expected to fail' \
+ | fail
+ elif (( $# > 0 )) && (( status != expected )); then
+ { local -ir width=8
+ batslib_print_kv_single "$width" \
+ 'expected' "$expected" \
+ 'actual' "$status"
+ batslib_print_kv_single_or_multi "$width" \
+ 'output' "$output"
+ } | batslib_decorate 'command failed as expected, but status differs' \
+ | fail
+ fi
+}
+
+# Fail and display details if `$output' does not match the expected
+# output. The expected output can be specified either by the first
+# parameter or on the standard input.
+#
+# By default, literal matching is performed. The assertion fails if the
+# expected output does not equal `$output'. Details include both values.
+#
+# Option `--partial' enables partial matching. The assertion fails if
+# the expected substring cannot be found in `$output'.
+#
+# Option `--regexp' enables regular expression matching. The assertion
+# fails if the extended regular expression does not match `$output'. An
+# invalid regular expression causes an error to be displayed.
+#
+# It is an error to use partial and regular expression matching
+# simultaneously.
+#
+# Globals:
+# output
+# Options:
+# -p, --partial - partial matching
+# -e, --regexp - extended regular expression matching
+# Arguments:
+# $1 - [=STDIN] expected output
+# Returns:
+# 0 - expected matches the actual output
+# 1 - otherwise
+# Inputs:
+# STDIN - [=$1] expected output
+# Outputs:
+# STDERR - details, on failure
+# error message, on error
+assert_output() {
+ local -i is_mode_partial=0
+ local -i is_mode_regexp=0
+
+ # Handle options.
+ while (( $# > 0 )); do
+ case "$1" in
+ -p|--partial) is_mode_partial=1; shift ;;
+ -e|--regexp) is_mode_regexp=1; shift ;;
+ --) shift; break ;;
+ *) break ;;
+ esac
+ done
+
+ if (( is_mode_partial )) && (( is_mode_regexp )); then
+ echo "\`--partial' and \`--regexp' are mutually exclusive" \
+ | batslib_decorate 'ERROR: assert_output' \
+ | fail
+ return $?
+ fi
+
+ # Arguments.
+ local expected
+ (( $# == 0 )) && expected="$(cat -)" || expected="$1"
+
+ # Matching.
+ if (( is_mode_regexp )); then
+ if [[ '' =~ $expected ]] || (( $? == 2 )); then
+ echo "Invalid extended regular expression: \`$expected'" \
+ | batslib_decorate 'ERROR: assert_output' \
+ | fail
+ return $?
+ fi
+ if ! [[ $output =~ $expected ]]; then
+ batslib_print_kv_single_or_multi 6 \
+ 'regexp' "$expected" \
+ 'output' "$output" \
+ | batslib_decorate 'regular expression does not match output' \
+ | fail
+ fi
+ elif (( is_mode_partial )); then
+ if [[ $output != *"$expected"* ]]; then
+ batslib_print_kv_single_or_multi 9 \
+ 'substring' "$expected" \
+ 'output' "$output" \
+ | batslib_decorate 'output does not contain substring' \
+ | fail
+ fi
+ else
+ if [[ $output != "$expected" ]]; then
+ batslib_print_kv_single_or_multi 8 \
+ 'expected' "$expected" \
+ 'actual' "$output" \
+ | batslib_decorate 'output differs' \
+ | fail
+ fi
+ fi
+}
+
+# Fail and display details if `$output' matches the unexpected output.
+# The unexpected output can be specified either by the first parameter
+# or on the standard input.
+#
+# By default, literal matching is performed. The assertion fails if the
+# unexpected output equals `$output'. Details include `$output'.
+#
+# Option `--partial' enables partial matching. The assertion fails if
+# the unexpected substring is found in `$output'. The unexpected
+# substring is added to details.
+#
+# Option `--regexp' enables regular expression matching. The assertion
+# fails if the extended regular expression does matches `$output'. The
+# regular expression is added to details. An invalid regular expression
+# causes an error to be displayed.
+#
+# It is an error to use partial and regular expression matching
+# simultaneously.
+#
+# Globals:
+# output
+# Options:
+# -p, --partial - partial matching
+# -e, --regexp - extended regular expression matching
+# Arguments:
+# $1 - [=STDIN] unexpected output
+# Returns:
+# 0 - unexpected matches the actual output
+# 1 - otherwise
+# Inputs:
+# STDIN - [=$1] unexpected output
+# Outputs:
+# STDERR - details, on failure
+# error message, on error
+refute_output() {
+ local -i is_mode_partial=0
+ local -i is_mode_regexp=0
+
+ # Handle options.
+ while (( $# > 0 )); do
+ case "$1" in
+ -p|--partial) is_mode_partial=1; shift ;;
+ -e|--regexp) is_mode_regexp=1; shift ;;
+ --) shift; break ;;
+ *) break ;;
+ esac
+ done
+
+ if (( is_mode_partial )) && (( is_mode_regexp )); then
+ echo "\`--partial' and \`--regexp' are mutually exclusive" \
+ | batslib_decorate 'ERROR: refute_output' \
+ | fail
+ return $?
+ fi
+
+ # Arguments.
+ local unexpected
+ (( $# == 0 )) && unexpected="$(cat -)" || unexpected="$1"
+
+ if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
+ echo "Invalid extended regular expression: \`$unexpected'" \
+ | batslib_decorate 'ERROR: refute_output' \
+ | fail
+ return $?
+ fi
+
+ # Matching.
+ if (( is_mode_regexp )); then
+ if [[ $output =~ $unexpected ]] || (( $? == 0 )); then
+ batslib_print_kv_single_or_multi 6 \
+ 'regexp' "$unexpected" \
+ 'output' "$output" \
+ | batslib_decorate 'regular expression should not match output' \
+ | fail
+ fi
+ elif (( is_mode_partial )); then
+ if [[ $output == *"$unexpected"* ]]; then
+ batslib_print_kv_single_or_multi 9 \
+ 'substring' "$unexpected" \
+ 'output' "$output" \
+ | batslib_decorate 'output should not contain substring' \
+ | fail
+ fi
+ else
+ if [[ $output == "$unexpected" ]]; then
+ batslib_print_kv_single_or_multi 6 \
+ 'output' "$output" \
+ | batslib_decorate 'output equals, but it was expected to differ' \
+ | fail
+ fi
+ fi
+}
+
+# Fail and display details if the expected line is not found in the
+# output (default) or in a specific line of it.
+#
+# By default, the entire output is searched for the expected line. The
+# expected line is matched against every element of `${lines[@]}'. If no
+# match is found, the assertion fails. Details include the expected line
+# and `${lines[@]}'.
+#
+# When `--index ' is specified, only the -th line is matched.
+# If the expected line does not match `${lines[]}', the assertion
+# fails. Details include and the compared lines.
+#
+# By default, literal matching is performed. A literal match fails if
+# the expected string does not equal the matched string.
+#
+# Option `--partial' enables partial matching. A partial match fails if
+# the expected substring is not found in the target string.
+#
+# Option `--regexp' enables regular expression matching. A regular
+# expression match fails if the extended regular expression does not
+# match the target string. An invalid regular expression causes an error
+# to be displayed.
+#
+# It is an error to use partial and regular expression matching
+# simultaneously.
+#
+# Mandatory arguments to long options are mandatory for short options
+# too.
+#
+# Globals:
+# output
+# lines
+# Options:
+# -n, --index - match the -th line
+# -p, --partial - partial matching
+# -e, --regexp - extended regular expression matching
+# Arguments:
+# $1 - expected line
+# Returns:
+# 0 - match found
+# 1 - otherwise
+# Outputs:
+# STDERR - details, on failure
+# error message, on error
+# FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
+assert_line() {
+ local -i is_match_line=0
+ local -i is_mode_partial=0
+ local -i is_mode_regexp=0
+
+ # Handle options.
+ while (( $# > 0 )); do
+ case "$1" in
+ -n|--index)
+ if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
+ echo "\`--index' requires an integer argument: \`$2'" \
+ | batslib_decorate 'ERROR: assert_line' \
+ | fail
+ return $?
+ fi
+ is_match_line=1
+ local -ri idx="$2"
+ shift 2
+ ;;
+ -p|--partial) is_mode_partial=1; shift ;;
+ -e|--regexp) is_mode_regexp=1; shift ;;
+ --) shift; break ;;
+ *) break ;;
+ esac
+ done
+
+ if (( is_mode_partial )) && (( is_mode_regexp )); then
+ echo "\`--partial' and \`--regexp' are mutually exclusive" \
+ | batslib_decorate 'ERROR: assert_line' \
+ | fail
+ return $?
+ fi
+
+ # Arguments.
+ local -r expected="$1"
+
+ if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
+ echo "Invalid extended regular expression: \`$expected'" \
+ | batslib_decorate 'ERROR: assert_line' \
+ | fail
+ return $?
+ fi
+
+ # Matching.
+ if (( is_match_line )); then
+ # Specific line.
+ if (( is_mode_regexp )); then
+ if ! [[ ${lines[$idx]} =~ $expected ]]; then
+ batslib_print_kv_single 6 \
+ 'index' "$idx" \
+ 'regexp' "$expected" \
+ 'line' "${lines[$idx]}" \
+ | batslib_decorate 'regular expression does not match line' \
+ | fail
+ fi
+ elif (( is_mode_partial )); then
+ if [[ ${lines[$idx]} != *"$expected"* ]]; then
+ batslib_print_kv_single 9 \
+ 'index' "$idx" \
+ 'substring' "$expected" \
+ 'line' "${lines[$idx]}" \
+ | batslib_decorate 'line does not contain substring' \
+ | fail
+ fi
+ else
+ if [[ ${lines[$idx]} != "$expected" ]]; then
+ batslib_print_kv_single 8 \
+ 'index' "$idx" \
+ 'expected' "$expected" \
+ 'actual' "${lines[$idx]}" \
+ | batslib_decorate 'line differs' \
+ | fail
+ fi
+ fi
+ else
+ # Contained in output.
+ if (( is_mode_regexp )); then
+ local -i idx
+ for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
+ [[ ${lines[$idx]} =~ $expected ]] && return 0
+ done
+ { local -ar single=(
+ 'regexp' "$expected"
+ )
+ local -ar may_be_multi=(
+ 'output' "$output"
+ )
+ local -ir width="$( batslib_get_max_single_line_key_width \
+ "${single[@]}" "${may_be_multi[@]}" )"
+ batslib_print_kv_single "$width" "${single[@]}"
+ batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
+ } | batslib_decorate 'no output line matches regular expression' \
+ | fail
+ elif (( is_mode_partial )); then
+ local -i idx
+ for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
+ [[ ${lines[$idx]} == *"$expected"* ]] && return 0
+ done
+ { local -ar single=(
+ 'substring' "$expected"
+ )
+ local -ar may_be_multi=(
+ 'output' "$output"
+ )
+ local -ir width="$( batslib_get_max_single_line_key_width \
+ "${single[@]}" "${may_be_multi[@]}" )"
+ batslib_print_kv_single "$width" "${single[@]}"
+ batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
+ } | batslib_decorate 'no output line contains substring' \
+ | fail
+ else
+ local -i idx
+ for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
+ [[ ${lines[$idx]} == "$expected" ]] && return 0
+ done
+ { local -ar single=(
+ 'line' "$expected"
+ )
+ local -ar may_be_multi=(
+ 'output' "$output"
+ )
+ local -ir width="$( batslib_get_max_single_line_key_width \
+ "${single[@]}" "${may_be_multi[@]}" )"
+ batslib_print_kv_single "$width" "${single[@]}"
+ batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
+ } | batslib_decorate 'output does not contain line' \
+ | fail
+ fi
+ fi
+}
+
+# Fail and display details if the unexpected line is found in the output
+# (default) or in a specific line of it.
+#
+# By default, the entire output is searched for the unexpected line. The
+# unexpected line is matched against every element of `${lines[@]}'. If
+# a match is found, the assertion fails. Details include the unexpected
+# line, the index of the first match and `${lines[@]}' with the matching
+# line highlighted if `${lines[@]}' is longer than one line.
+#
+# When `--index ' is specified, only the -th line is matched.
+# If the unexpected line matches `${lines[]}', the assertion fails.
+# Details include and the unexpected line.
+#
+# By default, literal matching is performed. A literal match fails if
+# the unexpected string does not equal the matched string.
+#
+# Option `--partial' enables partial matching. A partial match fails if
+# the unexpected substring is found in the target string. When used with
+# `--index ', the unexpected substring is also displayed on
+# failure.
+#
+# Option `--regexp' enables regular expression matching. A regular
+# expression match fails if the extended regular expression matches the
+# target string. When used with `--index ', the regular expression
+# is also displayed on failure. An invalid regular expression causes an
+# error to be displayed.
+#
+# It is an error to use partial and regular expression matching
+# simultaneously.
+#
+# Mandatory arguments to long options are mandatory for short options
+# too.
+#
+# Globals:
+# output
+# lines
+# Options:
+# -n, --index - match the -th line
+# -p, --partial - partial matching
+# -e, --regexp - extended regular expression matching
+# Arguments:
+# $1 - unexpected line
+# Returns:
+# 0 - match not found
+# 1 - otherwise
+# Outputs:
+# STDERR - details, on failure
+# error message, on error
+# FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
+refute_line() {
+ local -i is_match_line=0
+ local -i is_mode_partial=0
+ local -i is_mode_regexp=0
+
+ # Handle options.
+ while (( $# > 0 )); do
+ case "$1" in
+ -n|--index)
+ if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
+ echo "\`--index' requires an integer argument: \`$2'" \
+ | batslib_decorate 'ERROR: refute_line' \
+ | fail
+ return $?
+ fi
+ is_match_line=1
+ local -ri idx="$2"
+ shift 2
+ ;;
+ -p|--partial) is_mode_partial=1; shift ;;
+ -e|--regexp) is_mode_regexp=1; shift ;;
+ --) shift; break ;;
+ *) break ;;
+ esac
+ done
+
+ if (( is_mode_partial )) && (( is_mode_regexp )); then
+ echo "\`--partial' and \`--regexp' are mutually exclusive" \
+ | batslib_decorate 'ERROR: refute_line' \
+ | fail
+ return $?
+ fi
+
+ # Arguments.
+ local -r unexpected="$1"
+
+ if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
+ echo "Invalid extended regular expression: \`$unexpected'" \
+ | batslib_decorate 'ERROR: refute_line' \
+ | fail
+ return $?
+ fi
+
+ # Matching.
+ if (( is_match_line )); then
+ # Specific line.
+ if (( is_mode_regexp )); then
+ if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then
+ batslib_print_kv_single 6 \
+ 'index' "$idx" \
+ 'regexp' "$unexpected" \
+ 'line' "${lines[$idx]}" \
+ | batslib_decorate 'regular expression should not match line' \
+ | fail
+ fi
+ elif (( is_mode_partial )); then
+ if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
+ batslib_print_kv_single 9 \
+ 'index' "$idx" \
+ 'substring' "$unexpected" \
+ 'line' "${lines[$idx]}" \
+ | batslib_decorate 'line should not contain substring' \
+ | fail
+ fi
+ else
+ if [[ ${lines[$idx]} == "$unexpected" ]]; then
+ batslib_print_kv_single 5 \
+ 'index' "$idx" \
+ 'line' "${lines[$idx]}" \
+ | batslib_decorate 'line should differ' \
+ | fail
+ fi
+ fi
+ else
+ # Line contained in output.
+ if (( is_mode_regexp )); then
+ local -i idx
+ for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
+ if [[ ${lines[$idx]} =~ $unexpected ]]; then
+ { local -ar single=(
+ 'regexp' "$unexpected"
+ 'index' "$idx"
+ )
+ local -a may_be_multi=(
+ 'output' "$output"
+ )
+ local -ir width="$( batslib_get_max_single_line_key_width \
+ "${single[@]}" "${may_be_multi[@]}" )"
+ batslib_print_kv_single "$width" "${single[@]}"
+ if batslib_is_single_line "${may_be_multi[1]}"; then
+ batslib_print_kv_single "$width" "${may_be_multi[@]}"
+ else
+ may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
+ | batslib_prefix \
+ | batslib_mark '>' "$idx" )"
+ batslib_print_kv_multi "${may_be_multi[@]}"
+ fi
+ } | batslib_decorate 'no line should match the regular expression' \
+ | fail
+ return $?
+ fi
+ done
+ elif (( is_mode_partial )); then
+ local -i idx
+ for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
+ if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
+ { local -ar single=(
+ 'substring' "$unexpected"
+ 'index' "$idx"
+ )
+ local -a may_be_multi=(
+ 'output' "$output"
+ )
+ local -ir width="$( batslib_get_max_single_line_key_width \
+ "${single[@]}" "${may_be_multi[@]}" )"
+ batslib_print_kv_single "$width" "${single[@]}"
+ if batslib_is_single_line "${may_be_multi[1]}"; then
+ batslib_print_kv_single "$width" "${may_be_multi[@]}"
+ else
+ may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
+ | batslib_prefix \
+ | batslib_mark '>' "$idx" )"
+ batslib_print_kv_multi "${may_be_multi[@]}"
+ fi
+ } | batslib_decorate 'no line should contain substring' \
+ | fail
+ return $?
+ fi
+ done
+ else
+ local -i idx
+ for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
+ if [[ ${lines[$idx]} == "$unexpected" ]]; then
+ { local -ar single=(
+ 'line' "$unexpected"
+ 'index' "$idx"
+ )
+ local -a may_be_multi=(
+ 'output' "$output"
+ )
+ local -ir width="$( batslib_get_max_single_line_key_width \
+ "${single[@]}" "${may_be_multi[@]}" )"
+ batslib_print_kv_single "$width" "${single[@]}"
+ if batslib_is_single_line "${may_be_multi[1]}"; then
+ batslib_print_kv_single "$width" "${may_be_multi[@]}"
+ else
+ may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
+ | batslib_prefix \
+ | batslib_mark '>' "$idx" )"
+ batslib_print_kv_multi "${may_be_multi[@]}"
+ fi
+ } | batslib_decorate 'line should not be in output' \
+ | fail
+ return $?
+ fi
+ done
+ fi
+ fi
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-11-assert.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-11-assert.bats
new file mode 100755
index 0000000..6b7606b
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-11-assert.bats
@@ -0,0 +1,18 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'assert() : returns 0 if evaluates to TRUE' {
+ run assert true
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert() : returns 1 and displays if it evaluates to FALSE' {
+ run assert false
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- assertion failed --' ]
+ [ "${lines[1]}" == 'expression : false' ]
+ [ "${lines[2]}" == '--' ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-12-assert_equal.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-12-assert_equal.bats
new file mode 100755
index 0000000..b21725a
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-12-assert_equal.bats
@@ -0,0 +1,50 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'assert_equal() : returns 0 if equals ' {
+ run assert_equal 'a' 'a'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_equal() : returns 1 and displays details if does not equal ' {
+ run assert_equal 'a' 'b'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- values do not equal --' ]
+ [ "${lines[1]}" == 'expected : b' ]
+ [ "${lines[2]}" == 'actual : a' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+@test 'assert_equal() : displays details in multi-line format if is longer than one line' {
+ run assert_equal $'a 0\na 1' 'b'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- values do not equal --' ]
+ [ "${lines[1]}" == 'expected (1 lines):' ]
+ [ "${lines[2]}" == ' b' ]
+ [ "${lines[3]}" == 'actual (2 lines):' ]
+ [ "${lines[4]}" == ' a 0' ]
+ [ "${lines[5]}" == ' a 1' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+@test 'assert_equal() : displays details in multi-line format if is longer than one line' {
+ run assert_equal 'a' $'b 0\nb 1'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- values do not equal --' ]
+ [ "${lines[1]}" == 'expected (2 lines):' ]
+ [ "${lines[2]}" == ' b 0' ]
+ [ "${lines[3]}" == ' b 1' ]
+ [ "${lines[4]}" == 'actual (1 lines):' ]
+ [ "${lines[5]}" == ' a' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+@test 'assert_equal() : performs literal matching' {
+ run assert_equal 'a' '*'
+ [ "$status" -eq 1 ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-13-assert_success.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-13-assert_success.bats
new file mode 100755
index 0000000..6e80caa
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-13-assert_success.bats
@@ -0,0 +1,36 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test "assert_success(): returns 0 if \`\$status' is 0" {
+ run true
+ run assert_success
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_success(): returns 1 and displays details if \`\$status' is not 0" {
+ run bash -c 'echo "a"
+ exit 1'
+ run assert_success
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- command failed --' ]
+ [ "${lines[1]}" == 'status : 1' ]
+ [ "${lines[2]}" == 'output : a' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+@test "assert_success(): displays \`\$output' in multi-line format if it is longer than one line" {
+ run bash -c 'printf "a 0\na 1"
+ exit 1'
+ run assert_success
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 6 ]
+ [ "${lines[0]}" == '-- command failed --' ]
+ [ "${lines[1]}" == 'status : 1' ]
+ [ "${lines[2]}" == 'output (2 lines):' ]
+ [ "${lines[3]}" == ' a 0' ]
+ [ "${lines[4]}" == ' a 1' ]
+ [ "${lines[5]}" == '--' ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-14-assert_failure.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-14-assert_failure.bats
new file mode 100755
index 0000000..fee9685
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-14-assert_failure.bats
@@ -0,0 +1,69 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test "assert_failure(): returns 0 if \`\$status' is not 0" {
+ run false
+ run assert_failure
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_failure(): returns 1 and displays details if \`\$status' is 0" {
+ run bash -c 'echo "a"
+ exit 0'
+ run assert_failure
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- command succeeded, but it was expected to fail --' ]
+ [ "${lines[1]}" == 'output : a' ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test "assert_failure(): displays \`\$output' in multi-line format if it is longer then one line" {
+ run bash -c 'printf "a 0\na 1"
+ exit 0'
+ run assert_failure
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- command succeeded, but it was expected to fail --' ]
+ [ "${lines[1]}" == 'output (2 lines):' ]
+ [ "${lines[2]}" == ' a 0' ]
+ [ "${lines[3]}" == ' a 1' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+@test "assert_failure() : returns 0 if \`\$status' equals " {
+ run bash -c 'exit 1'
+ run assert_failure 1
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_failure() : returns 1 and displays details if \`\$status' does not equal " {
+ run bash -c 'echo "a"
+ exit 1'
+ run assert_failure 2
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- command failed as expected, but status differs --' ]
+ [ "${lines[1]}" == 'expected : 2' ]
+ [ "${lines[2]}" == 'actual : 1' ]
+ [ "${lines[3]}" == 'output : a' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+@test "assert_failure() : displays \`\$output' in multi-line format if it is longer then one line" {
+ run bash -c 'printf "a 0\na 1"
+ exit 1'
+ run assert_failure 2
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- command failed as expected, but status differs --' ]
+ [ "${lines[1]}" == 'expected : 2' ]
+ [ "${lines[2]}" == 'actual : 1' ]
+ [ "${lines[3]}" == 'output (2 lines):' ]
+ [ "${lines[4]}" == ' a 0' ]
+ [ "${lines[5]}" == ' a 1' ]
+ [ "${lines[6]}" == '--' ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-15-assert_output.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-15-assert_output.bats
new file mode 100755
index 0000000..cca79cc
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-15-assert_output.bats
@@ -0,0 +1,242 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+
+#
+# Literal matching
+#
+
+# Correctness
+@test "assert_output() : returns 0 if equals \`\$output'" {
+ run echo 'a'
+ run assert_output 'a'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_output() : returns 1 and displays details if does not equal \`\$output'" {
+ run echo 'b'
+ run assert_output 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- output differs --' ]
+ [ "${lines[1]}" == 'expected : a' ]
+ [ "${lines[2]}" == 'actual : b' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+@test 'assert_output(): reads from STDIN' {
+ run echo 'a'
+ run assert_output <: displays details in multi-line format if \`\$output' is longer than one line" {
+ run printf 'b 0\nb 1'
+ run assert_output 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- output differs --' ]
+ [ "${lines[1]}" == 'expected (1 lines):' ]
+ [ "${lines[2]}" == ' a' ]
+ [ "${lines[3]}" == 'actual (2 lines):' ]
+ [ "${lines[4]}" == ' b 0' ]
+ [ "${lines[5]}" == ' b 1' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+@test 'assert_output() : displays details in multi-line format if is longer than one line' {
+ run echo 'b'
+ run assert_output $'a 0\na 1'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- output differs --' ]
+ [ "${lines[1]}" == 'expected (2 lines):' ]
+ [ "${lines[2]}" == ' a 0' ]
+ [ "${lines[3]}" == ' a 1' ]
+ [ "${lines[4]}" == 'actual (1 lines):' ]
+ [ "${lines[5]}" == ' b' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+# Options
+@test 'assert_output() : performs literal matching by default' {
+ run echo 'a'
+ run assert_output '*'
+ [ "$status" -eq 1 ]
+}
+
+
+#
+# Partial matching: `-p' and `--partial'
+#
+
+# Options
+test_p_partial () {
+ run echo 'abc'
+ run assert_output "$1" 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_output() -p : enables partial matching' {
+ test_p_partial -p
+}
+
+@test 'assert_output() --partial : enables partial matching' {
+ test_p_partial --partial
+}
+
+# Correctness
+@test "assert_output() --partial : returns 0 if is a substring in \`\$output'" {
+ run printf 'a\nb\nc'
+ run assert_output --partial 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_output() --partial : returns 1 and displays details if is not a substring in \`\$output'" {
+ run echo 'b'
+ run assert_output --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- output does not contain substring --' ]
+ [ "${lines[1]}" == 'substring : a' ]
+ [ "${lines[2]}" == 'output : b' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Output formatting
+@test "assert_output() --partial : displays details in multi-line format if \`\$output' is longer than one line" {
+ run printf 'b 0\nb 1'
+ run assert_output --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- output does not contain substring --' ]
+ [ "${lines[1]}" == 'substring (1 lines):' ]
+ [ "${lines[2]}" == ' a' ]
+ [ "${lines[3]}" == 'output (2 lines):' ]
+ [ "${lines[4]}" == ' b 0' ]
+ [ "${lines[5]}" == ' b 1' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+@test 'assert_output() --partial : displays details in multi-line format if is longer than one line' {
+ run echo 'b'
+ run assert_output --partial $'a 0\na 1'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- output does not contain substring --' ]
+ [ "${lines[1]}" == 'substring (2 lines):' ]
+ [ "${lines[2]}" == ' a 0' ]
+ [ "${lines[3]}" == ' a 1' ]
+ [ "${lines[4]}" == 'output (1 lines):' ]
+ [ "${lines[5]}" == ' b' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+
+#
+# Regular expression matching: `-e' and `--regexp'
+#
+
+# Options
+test_r_regexp () {
+ run echo 'abc'
+ run assert_output "$1" '^a'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_output() -e : enables regular expression matching' {
+ test_r_regexp -e
+}
+
+@test 'assert_output() --regexp : enables regular expression matching' {
+ test_r_regexp --regexp
+}
+
+# Correctness
+@test "assert_output() --regexp : returns 0 if matches \`\$output'" {
+ run printf 'a\nb\nc'
+ run assert_output --regexp '.*b.*'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_output() --regexp : returns 1 and displays details if does not match \`\$output'" {
+ run echo 'b'
+ run assert_output --regexp '.*a.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- regular expression does not match output --' ]
+ [ "${lines[1]}" == 'regexp : .*a.*' ]
+ [ "${lines[2]}" == 'output : b' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Output formatting
+@test "assert_output() --regexp : displays details in multi-line format if \`\$output' is longer than one line" {
+ run printf 'b 0\nb 1'
+ run assert_output --regexp '.*a.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- regular expression does not match output --' ]
+ [ "${lines[1]}" == 'regexp (1 lines):' ]
+ [ "${lines[2]}" == ' .*a.*' ]
+ [ "${lines[3]}" == 'output (2 lines):' ]
+ [ "${lines[4]}" == ' b 0' ]
+ [ "${lines[5]}" == ' b 1' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+@test 'assert_output() --regexp : displays details in multi-line format if is longer than one line' {
+ run echo 'b'
+ run assert_output --regexp $'.*a\nb.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- regular expression does not match output --' ]
+ [ "${lines[1]}" == 'regexp (2 lines):' ]
+ [ "${lines[2]}" == ' .*a' ]
+ [ "${lines[3]}" == ' b.*' ]
+ [ "${lines[4]}" == 'output (1 lines):' ]
+ [ "${lines[5]}" == ' b' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+# Error handling
+@test 'assert_output() --regexp : returns 1 and displays an error message if is not a valid extended regular expression' {
+ run assert_output --regexp '[.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: assert_output --' ]
+ [ "${lines[1]}" == "Invalid extended regular expression: \`[.*'" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+
+#
+# Common
+#
+
+@test "assert_output(): \`--partial' and \`--regexp' are mutually exclusive" {
+ run assert_output --partial --regexp
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: assert_output --' ]
+ [ "${lines[1]}" == "\`--partial' and \`--regexp' are mutually exclusive" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test "assert_output(): \`--' stops parsing options" {
+ run echo '-p'
+ run assert_output -- '-p'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-16-refute_output.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-16-refute_output.bats
new file mode 100755
index 0000000..5204301
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-16-refute_output.bats
@@ -0,0 +1,196 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+
+#
+# Literal matching
+#
+
+# Correctness
+@test "refute_output() : returns 0 if does not equal \`\$output'" {
+ run echo 'b'
+ run refute_output 'a'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_output() : returns 1 and displays details if equals \`\$output'" {
+ run echo 'a'
+ run refute_output 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- output equals, but it was expected to differ --' ]
+ [ "${lines[1]}" == 'output : a' ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test 'refute_output(): reads from STDIN' {
+ run echo 'a'
+ run refute_output <: displays details in multi-line format if necessary' {
+ run printf 'a 0\na 1'
+ run refute_output $'a 0\na 1'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- output equals, but it was expected to differ --' ]
+ [ "${lines[1]}" == 'output (2 lines):' ]
+ [ "${lines[2]}" == ' a 0' ]
+ [ "${lines[3]}" == ' a 1' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+# Options
+@test 'refute_output() : performs literal matching by default' {
+ run echo 'a'
+ run refute_output '*'
+ [ "$status" -eq 0 ]
+}
+
+
+#
+# Partial matching: `-p' and `--partial'
+#
+
+# Options
+test_p_partial () {
+ run echo 'abc'
+ run refute_output "$1" 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute_output() -p : enables partial matching' {
+ test_p_partial -p
+}
+
+@test 'refute_output() --partial : enables partial matching' {
+ test_p_partial --partial
+}
+
+# Correctness
+@test "refute_output() --partial : returns 0 if is not a substring in \`\$output'" {
+ run printf 'a\nb\nc'
+ run refute_output --partial 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_output() --partial : returns 1 and displays details if is a substring in \`\$output'" {
+ run echo 'a'
+ run refute_output --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- output should not contain substring --' ]
+ [ "${lines[1]}" == 'substring : a' ]
+ [ "${lines[2]}" == 'output : a' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Output formatting
+@test 'refute_output() --partial : displays details in multi-line format if necessary' {
+ run printf 'a 0\na 1'
+ run refute_output --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- output should not contain substring --' ]
+ [ "${lines[1]}" == 'substring (1 lines):' ]
+ [ "${lines[2]}" == ' a' ]
+ [ "${lines[3]}" == 'output (2 lines):' ]
+ [ "${lines[4]}" == ' a 0' ]
+ [ "${lines[5]}" == ' a 1' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+
+#
+# Regular expression matching: `-e' and `--regexp'
+#
+
+# Options
+test_r_regexp () {
+ run echo 'abc'
+ run refute_output "$1" '^d'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute_output() -e : enables regular expression matching' {
+ test_r_regexp -e
+}
+
+@test 'refute_output() --regexp : enables regular expression matching' {
+ test_r_regexp --regexp
+}
+
+# Correctness
+@test "refute_output() --regexp : returns 0 if does not match \`\$output'" {
+ run printf 'a\nb\nc'
+ run refute_output --regexp '.*d.*'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_output() --regexp : returns 1 and displays details if matches \`\$output'" {
+ run echo 'a'
+ run refute_output --regexp '.*a.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- regular expression should not match output --' ]
+ [ "${lines[1]}" == 'regexp : .*a.*' ]
+ [ "${lines[2]}" == 'output : a' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Output formatting
+@test 'refute_output() --regexp : displays details in multi-line format if necessary' {
+ run printf 'a 0\na 1'
+ run refute_output --regexp '.*a.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 7 ]
+ [ "${lines[0]}" == '-- regular expression should not match output --' ]
+ [ "${lines[1]}" == 'regexp (1 lines):' ]
+ [ "${lines[2]}" == ' .*a.*' ]
+ [ "${lines[3]}" == 'output (2 lines):' ]
+ [ "${lines[4]}" == ' a 0' ]
+ [ "${lines[5]}" == ' a 1' ]
+ [ "${lines[6]}" == '--' ]
+}
+
+# Error handling
+@test 'refute_output() --regexp : returns 1 and displays an error message if is not a valid extended regular expression' {
+ run refute_output --regexp '[.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: refute_output --' ]
+ [ "${lines[1]}" == "Invalid extended regular expression: \`[.*'" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+
+#
+# Common
+#
+
+@test "refute_output(): \`--partial' and \`--regexp' are mutually exclusive" {
+ run refute_output --partial --regexp
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: refute_output --' ]
+ [ "${lines[1]}" == "\`--partial' and \`--regexp' are mutually exclusive" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test "refute_output(): \`--' stops parsing options" {
+ run echo '--'
+ run refute_output -- '-p'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-17-assert_line.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-17-assert_line.bats
new file mode 100755
index 0000000..bee3850
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-17-assert_line.bats
@@ -0,0 +1,334 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+
+###############################################################################
+# Containing a line
+###############################################################################
+
+#
+# Literal matching
+#
+
+# Correctness
+@test "assert_line() : returns 0 if is a line in \`\${lines[@]}'" {
+ run printf 'a\nb\nc'
+ run assert_line 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_line() : returns 1 and displays details if is not a line in \`\${lines[@]}'" {
+ run echo 'b'
+ run assert_line 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- output does not contain line --' ]
+ [ "${lines[1]}" == 'line : a' ]
+ [ "${lines[2]}" == 'output : b' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Output formatting
+@test "assert_line() : displays \`\$output' in multi-line format if it is longer than one line" {
+ run printf 'b 0\nb 1'
+ run assert_line 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 6 ]
+ [ "${lines[0]}" == '-- output does not contain line --' ]
+ [ "${lines[1]}" == 'line : a' ]
+ [ "${lines[2]}" == 'output (2 lines):' ]
+ [ "${lines[3]}" == ' b 0' ]
+ [ "${lines[4]}" == ' b 1' ]
+ [ "${lines[5]}" == '--' ]
+}
+
+# Options
+@test 'assert_line() : performs literal matching by default' {
+ run echo 'a'
+ run assert_line '*'
+ [ "$status" -eq 1 ]
+}
+
+
+#
+# Partial matching: `-p' and `--partial'
+#
+
+# Options
+test_p_partial () {
+ run printf 'a\n_b_\nc'
+ run assert_line "$1" 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_line() -p : enables partial matching' {
+ test_p_partial -p
+}
+
+@test 'assert_line() --partial : enables partial matching' {
+ test_p_partial --partial
+}
+
+# Correctness
+@test "assert_line() --partial : returns 0 if is a substring in any line in \`\${lines[@]}'" {
+ run printf 'a\n_b_\nc'
+ run assert_line --partial 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_line() --partial : returns 1 and displays details if is not a substring in any lines in \`\${lines[@]}'" {
+ run echo 'b'
+ run assert_line --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- no output line contains substring --' ]
+ [ "${lines[1]}" == 'substring : a' ]
+ [ "${lines[2]}" == 'output : b' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Output formatting
+@test "assert_line() --partial : displays \`\$output' in multi-line format if it is longer than one line" {
+ run printf 'b 0\nb 1'
+ run assert_line --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 6 ]
+ [ "${lines[0]}" == '-- no output line contains substring --' ]
+ [ "${lines[1]}" == 'substring : a' ]
+ [ "${lines[2]}" == 'output (2 lines):' ]
+ [ "${lines[3]}" == ' b 0' ]
+ [ "${lines[4]}" == ' b 1' ]
+ [ "${lines[5]}" == '--' ]
+}
+
+
+#
+# Regular expression matching: `-e' and `--regexp'
+#
+
+# Options
+test_r_regexp () {
+ run printf 'a\n_b_\nc'
+ run assert_line "$1" '^.b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_line() -e : enables regular expression matching' {
+ test_r_regexp -e
+}
+
+@test 'assert_line() --regexp : enables regular expression matching' {
+ test_r_regexp --regexp
+}
+
+# Correctness
+@test "assert_line() --regexp : returns 0 if matches any line in \`\${lines[@]}'" {
+ run printf 'a\n_b_\nc'
+ run assert_line --regexp '^.b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_line() --regexp : returns 1 and displays details if does not match any lines in \`\${lines[@]}'" {
+ run echo 'b'
+ run assert_line --regexp '^.a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- no output line matches regular expression --' ]
+ [ "${lines[1]}" == 'regexp : ^.a' ]
+ [ "${lines[2]}" == 'output : b' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Output formatting
+@test "assert_line() --regexp : displays \`\$output' in multi-line format if longer than one line" {
+ run printf 'b 0\nb 1'
+ run assert_line --regexp '^.a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 6 ]
+ [ "${lines[0]}" == '-- no output line matches regular expression --' ]
+ [ "${lines[1]}" == 'regexp : ^.a' ]
+ [ "${lines[2]}" == 'output (2 lines):' ]
+ [ "${lines[3]}" == ' b 0' ]
+ [ "${lines[4]}" == ' b 1' ]
+ [ "${lines[5]}" == '--' ]
+}
+
+
+###############################################################################
+# Matching single line: `-n' and `--index'
+###############################################################################
+
+# Options
+test_n_index () {
+ run printf 'a\nb\nc'
+ run assert_line "$1" 1 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_line() -n : matches against the -th line only' {
+ test_n_index -n
+}
+
+@test 'assert_line() --index : matches against the -th line only' {
+ test_n_index --index
+}
+
+@test 'assert_line() --index : returns 1 and displays an error message if is not an integer' {
+ run assert_line --index 1a
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: assert_line --' ]
+ [ "${lines[1]}" == "\`--index' requires an integer argument: \`1a'" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+
+#
+# Literal matching
+#
+
+# Correctness
+@test "assert_line() --index : returns 0 if equals \`\${lines[]}'" {
+ run printf 'a\nb\nc'
+ run assert_line --index 1 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_line() --index : returns 1 and displays details if does not equal \`\${lines[]}'" {
+ run printf 'a\nb\nc'
+ run assert_line --index 1 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- line differs --' ]
+ [ "${lines[1]}" == 'index : 1' ]
+ [ "${lines[2]}" == 'expected : a' ]
+ [ "${lines[3]}" == 'actual : b' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+# Options
+@test 'assert_line() --index : performs literal matching by default' {
+ run printf 'a\nb\nc'
+ run assert_line --index 1 '*'
+ [ "$status" -eq 1 ]
+}
+
+
+#
+# Partial matching: `-p' and `--partial'
+#
+
+# Options
+test_index_p_partial () {
+ run printf 'a\n_b_\nc'
+ run assert_line --index 1 "$1" 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_line() --index -p : enables partial matching' {
+ test_index_p_partial -p
+}
+
+@test 'assert_line() --index --partial : enables partial matching' {
+ test_index_p_partial --partial
+}
+
+# Correctness
+@test "assert_line() --index --partial : returns 0 if is a substring in \`\${lines[]}'" {
+ run printf 'a\n_b_\nc'
+ run assert_line --index 1 --partial 'b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_line() --index --partial : returns 1 and displays details if is not a substring in \`\${lines[]}'" {
+ run printf 'b 0\nb 1'
+ run assert_line --index 1 --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- line does not contain substring --' ]
+ [ "${lines[1]}" == 'index : 1' ]
+ [ "${lines[2]}" == 'substring : a' ]
+ [ "${lines[3]}" == 'line : b 1' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+
+#
+# Regular expression matching: `-e' and `--regexp'
+#
+
+# Options
+test_index_r_regexp () {
+ run printf 'a\n_b_\nc'
+ run assert_line --index 1 "$1" '^.b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'assert_line() --index -e : enables regular expression matching' {
+ test_index_r_regexp -e
+}
+
+@test 'assert_line() --index --regexp : enables regular expression matching' {
+ test_index_r_regexp --regexp
+}
+
+# Correctness
+@test "assert_line() --index --regexp : returns 0 if matches \`\${lines[]}'" {
+ run printf 'a\n_b_\nc'
+ run assert_line --index 1 --regexp '^.b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "assert_line() --index --regexp : returns 1 and displays details if does not match \`\${lines[]}'" {
+ run printf 'a\nb\nc'
+ run assert_line --index 1 --regexp '^.a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- regular expression does not match line --' ]
+ [ "${lines[1]}" == 'index : 1' ]
+ [ "${lines[2]}" == 'regexp : ^.a' ]
+ [ "${lines[3]}" == 'line : b' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+
+###############################################################################
+# Common
+###############################################################################
+
+@test "assert_line(): \`--partial' and \`--regexp' are mutually exclusive" {
+ run assert_line --partial --regexp
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: assert_line --' ]
+ [ "${lines[1]}" == "\`--partial' and \`--regexp' are mutually exclusive" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test 'assert_line() --regexp : returns 1 and displays an error message if is not a valid extended regular expression' {
+ run assert_line --regexp '[.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: assert_line --' ]
+ [ "${lines[1]}" == "Invalid extended regular expression: \`[.*'" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test "assert_line(): \`--' stops parsing options" {
+ run printf 'a\n-p\nc'
+ run assert_line -- '-p'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-18-refute_line.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-18-refute_line.bats
new file mode 100755
index 0000000..9cc8185
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-18-refute_line.bats
@@ -0,0 +1,342 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+
+###############################################################################
+# Containing a line
+###############################################################################
+
+#
+# Literal matching
+#
+
+# Correctness
+@test "refute_line() : returns 0 if is not a line in \`\${lines[@]}'" {
+ run printf 'a\nb\nc'
+ run refute_line 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_line() : returns 1 and displays details if is not a line in \`\${lines[@]}'" {
+ run echo 'a'
+ run refute_line 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- line should not be in output --' ]
+ [ "${lines[1]}" == 'line : a' ]
+ [ "${lines[2]}" == 'index : 0' ]
+ [ "${lines[3]}" == 'output : a' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+# Output formatting
+@test "refute_line() : displays \`\$output' in multi-line format if it is longer than one line" {
+ run printf 'a 0\na 1\na 2'
+ run refute_line 'a 1'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 8 ]
+ [ "${lines[0]}" == '-- line should not be in output --' ]
+ [ "${lines[1]}" == 'line : a 1' ]
+ [ "${lines[2]}" == 'index : 1' ]
+ [ "${lines[3]}" == 'output (3 lines):' ]
+ [ "${lines[4]}" == ' a 0' ]
+ [ "${lines[5]}" == '> a 1' ]
+ [ "${lines[6]}" == ' a 2' ]
+ [ "${lines[7]}" == '--' ]
+}
+
+# Options
+@test 'refute_line() : performs literal matching by default' {
+ run echo 'a'
+ run refute_line '*'
+ [ "$status" -eq 0 ]
+}
+
+
+#
+# Partial matching: `-p' and `--partial'
+#
+
+# Options
+test_p_partial () {
+ run printf 'a\nb\nc'
+ run refute_line "$1" 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute_line() -p : enables partial matching' {
+ test_p_partial -p
+}
+
+@test 'refute_line() --partial : enables partial matching' {
+ test_p_partial --partial
+}
+
+# Correctness
+@test "refute_line() --partial : returns 0 if is not a substring in any line in \`\${lines[@]}'" {
+ run printf 'a\nb\nc'
+ run refute_line --partial 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_line() --partial : returns 1 and displays details if is a substring in any line in \`\${lines[@]}'" {
+ run echo 'a'
+ run refute_line --partial 'a'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- no line should contain substring --' ]
+ [ "${lines[1]}" == 'substring : a' ]
+ [ "${lines[2]}" == 'index : 0' ]
+ [ "${lines[3]}" == 'output : a' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+# Output formatting
+@test "refute_line() --partial : displays \`\$output' in multi-line format if it is longer than one line" {
+ run printf 'a\nabc\nc'
+ run refute_line --partial 'b'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 8 ]
+ [ "${lines[0]}" == '-- no line should contain substring --' ]
+ [ "${lines[1]}" == 'substring : b' ]
+ [ "${lines[2]}" == 'index : 1' ]
+ [ "${lines[3]}" == 'output (3 lines):' ]
+ [ "${lines[4]}" == ' a' ]
+ [ "${lines[5]}" == '> abc' ]
+ [ "${lines[6]}" == ' c' ]
+ [ "${lines[7]}" == '--' ]
+}
+
+
+#
+# Regular expression matching: `-e' and `--regexp'
+#
+
+# Options
+test_r_regexp () {
+ run printf 'a\nb\nc'
+ run refute_line "$1" '^.d'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute_line() -e : enables regular expression matching' {
+ test_r_regexp -e
+}
+
+@test 'refute_line() --regexp : enables regular expression matching' {
+ test_r_regexp --regexp
+}
+
+# Correctness
+@test "refute_line() --regexp : returns 0 if does not match any line in \`\${lines[@]}'" {
+ run printf 'a\nb\nc'
+ run refute_line --regexp '.*d.*'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_line() --regexp : returns 1 and displays details if matches any lines in \`\${lines[@]}'" {
+ run echo 'a'
+ run refute_line --regexp '.*a.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- no line should match the regular expression --' ]
+ [ "${lines[1]}" == 'regexp : .*a.*' ]
+ [ "${lines[2]}" == 'index : 0' ]
+ [ "${lines[3]}" == 'output : a' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+# Output formatting
+@test "refute_line() --regexp : displays \`\$output' in multi-line format if longer than one line" {
+ run printf 'a\nabc\nc'
+ run refute_line --regexp '.*b.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 8 ]
+ [ "${lines[0]}" == '-- no line should match the regular expression --' ]
+ [ "${lines[1]}" == 'regexp : .*b.*' ]
+ [ "${lines[2]}" == 'index : 1' ]
+ [ "${lines[3]}" == 'output (3 lines):' ]
+ [ "${lines[4]}" == ' a' ]
+ [ "${lines[5]}" == '> abc' ]
+ [ "${lines[6]}" == ' c' ]
+ [ "${lines[7]}" == '--' ]
+}
+
+
+###############################################################################
+# Matching single line: `-n' and `--index'
+###############################################################################
+
+# Options
+test_n_index () {
+ run printf 'a\nb\nc'
+ run refute_line "$1" 1 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute_line() -n : matches against the -th line only' {
+ test_n_index -n
+}
+
+@test 'refute_line() --index : matches against the -th line only' {
+ test_n_index --index
+}
+
+@test 'refute_line() --index : returns 1 and displays an error message if is not an integer' {
+ run refute_line --index 1a
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: refute_line --' ]
+ [ "${lines[1]}" == "\`--index' requires an integer argument: \`1a'" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+
+#
+# Literal matching
+#
+
+# Correctness
+@test "refute_line() --index : returns 0 if does not equal \`\${lines[]}'" {
+ run printf 'a\nb\nc'
+ run refute_line --index 1 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_line() --index : returns 1 and displays details if equals \`\${lines[]}'" {
+ run printf 'a\nb\nc'
+ run refute_line --index 1 'b'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 4 ]
+ [ "${lines[0]}" == '-- line should differ --' ]
+ [ "${lines[1]}" == 'index : 1' ]
+ [ "${lines[2]}" == 'line : b' ]
+ [ "${lines[3]}" == '--' ]
+}
+
+# Options
+@test 'refute_line() --index : performs literal matching by default' {
+ run printf 'a\nb\nc'
+ run refute_line --index 1 '*'
+ [ "$status" -eq 0 ]
+}
+
+
+#
+# Partial matching: `-p' and `--partial'
+#
+
+# Options
+test_index_p_partial () {
+ run printf 'a\nb\nc'
+ run refute_line --index 1 "$1" 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute_line() --index -p : enables partial matching' {
+ test_index_p_partial -p
+}
+
+@test 'refute_line() --index --partial : enables partial matching' {
+ test_index_p_partial --partial
+}
+
+# Correctness
+@test "refute_line() --index --partial : returns 0 if is not a substring in \`\${lines[]}'" {
+ run printf 'a\nabc\nc'
+ run refute_line --index 1 --partial 'd'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_line() --index --partial : returns 1 and displays details if is a substring in \`\${lines[]}'" {
+ run printf 'a\nabc\nc'
+ run refute_line --index 1 --partial 'b'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- line should not contain substring --' ]
+ [ "${lines[1]}" == 'index : 1' ]
+ [ "${lines[2]}" == 'substring : b' ]
+ [ "${lines[3]}" == 'line : abc' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+
+#
+# Regular expression matching: `-e' and `--regexp'
+#
+
+# Options
+test_index_r_regexp () {
+ run printf 'a\nb\nc'
+ run refute_line --index 1 "$1" '^.b'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute_line() --index -e : enables regular expression matching' {
+ test_index_r_regexp -e
+}
+
+@test 'refute_line() --index --regexp : enables regular expression matching' {
+ test_index_r_regexp --regexp
+}
+
+# Correctness
+@test "refute_line() --index --regexp : returns 0 if does not match \`\${lines[]}'" {
+ run printf 'a\nabc\nc'
+ run refute_line --index 1 --regexp '.*d.*'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test "refute_line() --index --regexp : returns 1 and displays details if matches \`\${lines[]}'" {
+ run printf 'a\nabc\nc'
+ run refute_line --index 1 --regexp '.*b.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 5 ]
+ [ "${lines[0]}" == '-- regular expression should not match line --' ]
+ [ "${lines[1]}" == 'index : 1' ]
+ [ "${lines[2]}" == 'regexp : .*b.*' ]
+ [ "${lines[3]}" == 'line : abc' ]
+ [ "${lines[4]}" == '--' ]
+}
+
+
+###############################################################################
+# Common
+###############################################################################
+
+@test "refute_line(): \`--partial' and \`--regexp' are mutually exclusive" {
+ run refute_line --partial --regexp
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: refute_line --' ]
+ [ "${lines[1]}" == "\`--partial' and \`--regexp' are mutually exclusive" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test 'refute_line() --regexp : returns 1 and displays an error message if is not a valid extended regular expression' {
+ run refute_line --regexp '[.*'
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- ERROR: refute_line --' ]
+ [ "${lines[1]}" == "Invalid extended regular expression: \`[.*'" ]
+ [ "${lines[2]}" == '--' ]
+}
+
+@test "refute_line(): \`--' stops parsing options" {
+ run printf 'a\n--\nc'
+ run refute_line -- '-p'
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-19-refute.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-19-refute.bats
new file mode 100755
index 0000000..191dc73
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/50-assert-19-refute.bats
@@ -0,0 +1,18 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'refute() : returns 0 if evaluates to FALSE' {
+ run refute false
+ [ "$status" -eq 0 ]
+ [ "${#lines[@]}" -eq 0 ]
+}
+
+@test 'refute() : returns 1 and displays if it evaluates to TRUE' {
+ run refute true
+ [ "$status" -eq 1 ]
+ [ "${#lines[@]}" -eq 3 ]
+ [ "${lines[0]}" == '-- assertion succeeded, but it was expected to fail --' ]
+ [ "${lines[1]}" == 'expression : true' ]
+ [ "${lines[2]}" == '--' ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/test_helper.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/test_helper.bash
new file mode 100644
index 0000000..32ad846
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-assert/test/test_helper.bash
@@ -0,0 +1,10 @@
+setup() {
+ export TEST_MAIN_DIR="${BATS_TEST_DIRNAME}/.."
+ export TEST_DEPS_DIR="${TEST_DEPS_DIR-${TEST_MAIN_DIR}/..}"
+
+ # Load dependencies.
+ load "${TEST_DEPS_DIR}/bats-support/load.bash"
+
+ # Load library.
+ load '../load'
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/.travis.yml b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/.travis.yml
new file mode 100644
index 0000000..75721f2
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/.travis.yml
@@ -0,0 +1,7 @@
+language: bash
+before_install:
+ - ./script/install-bats.sh
+before_script:
+ - export PATH="${HOME}/.local/bin:${PATH}"
+script:
+ - bats test
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/CHANGELOG.md b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/CHANGELOG.md
new file mode 100644
index 0000000..324d247
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/CHANGELOG.md
@@ -0,0 +1,46 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+This project adheres to [Semantic Versioning](http://semver.org/).
+
+
+## [0.3.0] - 2016-11-29
+
+### Added
+
+- Restricting invocation to specific locations with
+ `batslib_is_caller()`
+
+
+## [0.2.0] - 2016-03-22
+
+### Added
+
+- `npm` support
+- Reporting arbitrary failures with `fail()` (moved from `bats-assert`)
+
+### Changed
+
+- Library renamed to `bats-support`
+
+
+## 0.1.0 - 2016-02-16
+
+### Added
+
+- Two-column key-value formatting with `batslib_print_kv_single()`
+- Multi-line key-value formatting with `batslib_print_kv_multi()`
+- Mixed formatting with `batslib_print_kv_single_or_multi()`
+- Header and footer decoration with `batslib_decorate()`
+- Prefixing lines with `batslib_prefix()`
+- Marking lines with `batslib_mark()`
+- Common output function `batslib_err()`
+- Line counting with `batslib_count_lines()`
+- Checking whether a text is one line long with
+ `batslib_is_single_line()`
+- Determining key width for two-column and mixed formatting with
+ `batslib_get_max_single_line_key_width()`
+
+
+[0.3.0]: https://github.com/ztombol/bats-support/compare/v0.2.0...v0.3.0
+[0.2.0]: https://github.com/ztombol/bats-support/compare/v0.1.0...v0.2.0
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/LICENSE b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/LICENSE
new file mode 100644
index 0000000..670154e
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/LICENSE
@@ -0,0 +1,116 @@
+CC0 1.0 Universal
+
+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.
+
+For more information, please see
+
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/README.md b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/README.md
new file mode 100644
index 0000000..71c02ba
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/README.md
@@ -0,0 +1,189 @@
+*__Important:__ `bats-core` has been renamed to `bats-support`. GitHub
+automatically redirects all references, e.g. submodules and clones will
+continue to work, but you are encouraged to [update][github-rename]
+them. Version numbering continues where `bats-core` left off.*
+
+[github-rename]: https://help.github.com/articles/renaming-a-repository/
+
+- - - - -
+
+# bats-support
+
+[](https://raw.githubusercontent.com/ztombol/bats-support/master/LICENSE)
+[](https://github.com/ztombol/bats-support/releases/latest)
+[](https://travis-ci.org/ztombol/bats-support)
+
+`bats-support` is a supporting library providing common functions to
+test helper libraries written for [Bats][bats].
+
+Features:
+- [error reporting](#error-reporting)
+- [output formatting](#output-formatting)
+- [language tools](#language-and-execution)
+
+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).
+
+
+## Error reporting
+
+### `fail`
+
+Display an error message and fail. This function provides a convenient
+way to report failure in arbitrary situations. You can use it to
+implement your own helpers when the ones available do not meet your
+needs. Other functions use it internally as well.
+
+```bash
+@test 'fail()' {
+ fail 'this test always fails'
+}
+```
+
+The message can also be specified on the standard input.
+
+```bash
+@test 'fail() with pipe' {
+ echo 'this test always fails' | fail
+}
+```
+
+This function always fails and simply outputs the given message.
+
+```
+this test always fails
+```
+
+
+## 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
+--
+```
+
+## Language and Execution
+
+### Restricting invocation to specific locations
+
+Sometimes a helper may work properly only when called from a certain
+location. Because it depends on variables to be set or some other side
+effect.
+
+A good example is cleaning up temporary files only if the test has
+succeeded. The outcome of a test is only available in `teardown`. Thus,
+to avoid programming mistakes, it makes sense to restrict such a
+clean-up helper to that function.
+
+`batslib_is_caller` checks the call stack and returns `0` if the caller
+was invoked from a given function, and `1` otherwise. This function
+becomes really useful with the `--indirect` option, which allows calls
+through intermediate functions, e.g. the calling function may be called
+from a function that was called from the given function.
+
+Staying with the example above, the following code snippet implements a
+helper that is restricted to `teardown` or any function called
+indirectly from it.
+
+```shell
+clean_up() {
+ # Check caller.
+ if batslib_is_caller --indirect 'teardown'; then
+ echo "Must be called from \`teardown'" \
+ | batslib_decorate 'ERROR: clean_up' \
+ | fail
+ return $?
+ fi
+
+ # Body goes here...
+}
+```
+
+In some cases a helper may be called from multiple locations. For
+example, a logging function that uses the test name, description or
+number, information only available in `setup`, `@test` or `teardown`, to
+distinguish entries. The following snippet implements this restriction.
+
+```shell
+log_test() {
+ # Check caller.
+ if ! ( batslib_is_caller --indirect 'setup' \
+ || batslib_is_caller --indirect "$BATS_TEST_NAME" \
+ || batslib_is_caller --indirect 'teardown' )
+ then
+ echo "Must be called from \`setup', \`@test' or \`teardown'" \
+ | batslib_decorate 'ERROR: log_test' \
+ | fail
+ return $?
+ fi
+
+ # Body goes here...
+}
+```
+
+
+
+
+[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/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/load.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/load.bash
new file mode 100644
index 0000000..0727aeb
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/load.bash
@@ -0,0 +1,3 @@
+source "$(dirname "${BASH_SOURCE[0]}")/src/output.bash"
+source "$(dirname "${BASH_SOURCE[0]}")/src/error.bash"
+source "$(dirname "${BASH_SOURCE[0]}")/src/lang.bash"
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/package.json b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/package.json
new file mode 100644
index 0000000..192d16a
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "bats-support",
+ "version": "0.3.0",
+ "private": true
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/script/install-bats.sh b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/script/install-bats.sh
new file mode 100755
index 0000000..4c3161a
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/script/install-bats.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -o errexit
+set -o xtrace
+
+git clone --depth 1 https://github.com/sstephenson/bats
+cd bats && ./install.sh "${HOME}/.local" && cd .. && rm -rf bats
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/error.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/error.bash
new file mode 100644
index 0000000..e5d9791
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/error.bash
@@ -0,0 +1,41 @@
+#
+# bats-support - Supporting library for Bats test helpers
+#
+# Written in 2016 by Zoltan Tombol
+#
+# 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
+# .
+#
+
+#
+# error.bash
+# ----------
+#
+# Functions implementing error reporting. Used by public helper
+# functions or test suits directly.
+#
+
+# Fail and display a message. When no parameters are specified, the
+# message is read from the standard input. Other functions use this to
+# report failure.
+#
+# Globals:
+# none
+# Arguments:
+# $@ - [=STDIN] message
+# Returns:
+# 1 - always
+# Inputs:
+# STDIN - [=$@] message
+# Outputs:
+# STDERR - message
+fail() {
+ (( $# == 0 )) && batslib_err || batslib_err "$@"
+ return 1
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/lang.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/lang.bash
new file mode 100644
index 0000000..c57e299
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/lang.bash
@@ -0,0 +1,73 @@
+#
+# bats-util - Various auxiliary functions for Bats
+#
+# Written in 2016 by Zoltan Tombol
+#
+# 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
+# .
+#
+
+#
+# lang.bash
+# ---------
+#
+# Bash language and execution related functions. Used by public helper
+# functions.
+#
+
+# Check whether the calling function was called from a given function.
+#
+# By default, direct invocation is checked. The function succeeds if the
+# calling function was called directly from the given function. In other
+# words, if the given function is the next element on the call stack.
+#
+# When `--indirect' is specified, indirect invocation is checked. The
+# function succeeds if the calling function was called from the given
+# function with any number of intermediate calls. In other words, if the
+# given function can be found somewhere on the call stack.
+#
+# Direct invocation is a form of indirect invocation with zero
+# intermediate calls.
+#
+# Globals:
+# FUNCNAME
+# Options:
+# -i, --indirect - check indirect invocation
+# Arguments:
+# $1 - calling function's name
+# Returns:
+# 0 - current function was called from the given function
+# 1 - otherwise
+batslib_is_caller() {
+ local -i is_mode_direct=1
+
+ # Handle options.
+ while (( $# > 0 )); do
+ case "$1" in
+ -i|--indirect) is_mode_direct=0; shift ;;
+ --) shift; break ;;
+ *) break ;;
+ esac
+ done
+
+ # Arguments.
+ local -r func="$1"
+
+ # Check call stack.
+ if (( is_mode_direct )); then
+ [[ $func == "${FUNCNAME[2]}" ]] && return 0
+ else
+ local -i depth
+ for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do
+ [[ $func == "${FUNCNAME[$depth]}" ]] && return 0
+ done
+ fi
+
+ return 1
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/output.bash b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/output.bash
new file mode 100644
index 0000000..c6cf6a6
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/src/output.bash
@@ -0,0 +1,279 @@
+#
+# bats-support - Supporting library for Bats test helpers
+#
+# Written in 2016 by Zoltan Tombol
+#
+# 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
+# .
+#
+
+#
+# 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/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-10-batslib_err.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-10-batslib_err.bats
new file mode 100755
index 0000000..8c27fd1
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-10-batslib_err.bats
@@ -0,0 +1,16 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_err() : displays ' {
+ run batslib_err 'm1' 'm2'
+ [ "$status" -eq 0 ]
+ [ "$output" == 'm1 m2' ]
+}
+
+@test 'batslib_err(): reads 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/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-11-batslib_count_lines.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-11-batslib_count_lines.bats
new file mode 100755
index 0000000..ea172c3
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-11-batslib_count_lines.bats
@@ -0,0 +1,21 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+@test 'batslib_count_lines() : displays the number of lines in ' {
+ run batslib_count_lines $'a\nb\nc\n'
+ [ "$status" -eq 0 ]
+ [ "$output" == '3' ]
+}
+
+@test 'batslib_count_lines() : 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() : counts empty lines' {
+ run batslib_count_lines $'\n\n\n'
+ [ "$status" -eq 0 ]
+ [ "$output" == '3' ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-12-batslib_is_single_line.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-12-batslib_is_single_line.bats
new file mode 100755
index 0000000..484b64d
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/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() : returns 0 if all are single-line' {
+ run batslib_is_single_line 'a' $'b\n' 'c'
+ [ "$status" -eq 0 ]
+}
+
+@test 'batslib_is_single_line() : returns 1 if at least one of is longer than one line' {
+ run batslib_is_single_line 'a' $'b\nb' 'c'
+ [ "$status" -eq 1 ]
+}
diff --git a/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-13-batslib_get_max_single_line_key_width.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-13-batslib_get_max_single_line_key_width.bats
new file mode 100755
index 0000000..e6af161
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/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() : 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() : 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/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-14-batslib_print_kv_single.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-14-batslib_print_kv_single.bats
new file mode 100755
index 0000000..7637897
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/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() : displays in two-column format with 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() : 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/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-15-batslib_print_kv_multi.bats b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/test/50-output-15-batslib_print_kv_multi.bats
new file mode 100755
index 0000000..6ad4b3d
--- /dev/null
+++ b/06-automation/06-02-tools-bats/src/test/bats/test_helper/bats-support/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() : displays