From 7ad9898e892509c76b3d2d475c9ff6256bc2d188 Mon Sep 17 00:00:00 2001 From: Erik Phillips Date: Fri, 3 Jun 2016 07:47:47 -0600 Subject: [PATCH 1/2] Inital unit testing. Adds support for testing with BATS. Adds documenation for contributors about running tests. Adds unit testing with 12 tests. 2 of which will be failing on the upstream nk412/optparse master, but this will be addressed in seprate pull requests. See documentation under CONTRIBUTORS.md. Simple tests use a wrapper and go in tests/optparse.run.bats, while more advanced testing is in tests/optparse.load.bats. Added a testcase for bash's nounset option, which will 'exit 1' when encountering an undefined variable. Optparse1 does not handle nounset properly Added testcase for default values with spaces and special character Added list of current known issues Moved run tests to optparse.run.bats, to reflect this scripts use RUN and not LOAD for testing Added bats load tests, which include optparse directly and build options in each test case, for more advanced test cases Updated docs for test cases Doc updates: added general process workflow, additional goals / features, fixed typo --- CONTRIBUTORS.md | 83 ++++++++++++++++++++++++++++++ tests/interface.bash | 64 +++++++++++++++++++++++ tests/optparse.load.bats | 42 +++++++++++++++ tests/optparse.run.bats | 107 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 CONTRIBUTORS.md create mode 100755 tests/interface.bash create mode 100644 tests/optparse.load.bats create mode 100644 tests/optparse.run.bats diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..eaa0a14 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,83 @@ +# Contributor / development notes + +## Known issues + +- ~~Breaks with nounset enabled~~ +- ~~Default values containing spaces breaks script~~ + +## Unit tests + +Tests are written using BATS (Bash Automated Testing System). On my Ubuntu +box, bats was available in APT, so just an *apt-get install bats* set it up for +me. YYMV, but check your package manager before installing from source. + +Bash Automated Testing System: +https://github.com/sstephenson/bats + +### Running test + +Tests must be run from the project root. All paths used in testing must also be +from the project root. + +```bash +bats tests/ +``` + +Should probably create a Makefile to do this someday...but it's pretty easy to +remember as is... + +### Debugging + +Test assertion failures should be printed right to the screen when running bats. +However, if bats encounters a syntax error when processing a sourced / included +file (which is pretty frequent), then you won't get any output + +#### If your test case fails, but you don't get a message + +Check for /tmp/bats.* files. If bats gets an error when sourcing / including other +files, it chokes & saves output under /tmp in a new file like bats..out + + +## Execution Workflow + +Just a description of how this script runs. It seemed a bit cryptic at first, but once you get +the idea, it's really straight forward. + +- Script is source and included as normal +- Calls to optparse.define are made: + - Each call runs a parser loop to turn key=val assignments into local variables + - local variables are short, shortname, long, longname, variable, default, val + - Next we build multi line strings. Which are actually bash code. They will later be written to a file and executed to do the parsing. + - **optparse_usage** + - Generates each help line for each define method + - **optparse_contrations** + - This builds the lookup section of a CASE statement below + - Used for converting longopts into shortopts + - Triggers the call to **usage** when --help is found + - Catch all / default will detect unrecognized options & throw an error + - In future versions, this should handle variable assignment, instead of relying on getopts. Which will also add longname only support without a shortname. + - **optparse_defaults** + - Sets up our local variable defaults + - In original optparse, only args with default values are specified here (No initializations) + - In new version, all variables defined are initialized here, with an empty string if no default has been specified. This fixes support for bash's nounset option + - **optparse_arguments_string** + - The getops shortname only argument string (like: "io:uay:p") + - This should be removed in upcoming versions... + - **optparse_process** + - This is the assignment done inside the getopts CASE statement. + - Handles assigning the user specified value to the local variable. + - In future versions, the logic here should be moved to the **optparse_contractions** segment. +- After all arguments are defined, we call .build or .run to do argument to variable assignments + - ```source $(optparse.build)``` + - In older versions, the .build method is used, to create a local temp file of valid bash code, which is then executed. + - In future versions, we will just return the code to be executed and run it using process substitution. ```source <(optparse.run)```. This saves us from creating a temp file on every run. + - For backwards compatibility, the build method will still be available. + - Currently, creates a temp file like /tmp/optparse..tmp: + - usage() definition + - optparse_contraction - assigns shortop codes when longopts are used + - ```eval set -- params``` # This sets our input parameters. Since our nested script isn't passed the shell arguments + - optparse_defaults - Default assignments & local variable initilization + - getopts processing (legacy) + - Assigns local variables to user specified args via getops CASE statement + - Removes the local optparse.tmp script, since it's not longer needed at this point. + diff --git a/tests/interface.bash b/tests/interface.bash new file mode 100755 index 0000000..58fa0f2 --- /dev/null +++ b/tests/interface.bash @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# This is a command line interface to optparse for testing. +# It just prints the input that was parsed after calling optparse.build +# 3 arguments are currently available. INPUT, OUTPUT and ATTRIB +# +# MODES +# Different modes can be activated using the environment variable +# OPTPARSE_TEST_MODE. Use export OPTPARSE_TEST_MODE="mode" to change. +# Possible values are: +# print - Just print input to output, with no requirements (Default) +# nounset - Set the nounset option (fail when accessing undefined variables), +# then call *print* like default +# require - Use some required options (not implemented yet...) + +# Path variables +SCRIPT_FILE=${0}; +TESTS_DIR=$(dirname $(realpath "${SCRIPT_FILE}")); +SRC_FILE=$(realpath "${TESTS_DIR}/../optparse.bash"); +source "${SRC_FILE}" || { echo "ERROR: Could not load script source." && exit 1; } + +# Check environment variable OPTPARSE_TEST_MODE for requested mode, +# and call mode_{requested_mode} function +main() { + + case "${OPTPARSE_TEST_MODE:=print}" in + require) + mode_require + ;; + nounset) + set -o nounset + mode_print "$@" + ;; + *) + mode_print "$@" + esac +} + +mode_require() { + echo "Mode require" +} + +mode_print() { + optparse.define short=i long=input desc="The input file. No default" required="true" variable=INPUT value="" + optparse.define short=o long=output desc="Output file. Default is default_value" variable=OUTPUT default="default_value" + optparse.define short=a long=attrib desc="Boolean style attribute." variable="ATTRIB" value="true" default="false" + optparse.define short=d long=default-value-with-spaces desc="An argument which has spaces in it's default value" variable=DEFAULT_WITH_SPACES default="default value with spaces" + optparse.define short=s long=default-value-with-specials desc="An argument with a few special characters in it. A single quote should be handled ok" variable=DEFAULT_WITH_SPECIALS default="this is ' the !@#$%^&*( \${P\} special values" + source $(optparse.build); + + echo "INPUT=${INPUT}"; + echo "OUTPUT=${OUTPUT}"; + echo "ATTRIB=${ATTRIB}"; + echo "DEFAULT_WITH_SPACES=${DEFAULT_WITH_SPACES}"; + echo "DEFAULT_WITH_SPECIALS=${DEFAULT_WITH_SPECIALS}" +} + +main "$@" + + + + + + diff --git a/tests/optparse.load.bats b/tests/optparse.load.bats new file mode 100644 index 0000000..6c4e798 --- /dev/null +++ b/tests/optparse.load.bats @@ -0,0 +1,42 @@ +#!/usr/bin/env bats + +############################################################################### +# These tests use the LOAD method bats to include optparse here and we add +# options in each test case. +# Command line arguments are specified by using ```eval set -- ``` +# Example: +# +# eval set -- -i "input" --output "whatever" -v +# +# **NOTE**: If your testcase fails without any output from bats, then there's +# likely syntax errors on an included file. Check under /tmp for bats.* files. +# The error output will be there. (Also might check for optparse.* files too) +############################################################################### + +@test "include optparse from test script" { + load ../optparse + #optparse.define short=t long=testing desc="Test attribute" variable="TESTATTRIB" value="true" default="false" + #eval set -- # Our command line arguments go here + file=$(optparse.build) + echo "${file}" # This echo will show filename if the tests fail + [ -e "${file}" -a -r "${file}" ] + + # Optparse file was created ok. Run bash -n to lint the file & verify we don't have any errors + run bash -n "${file}" # Lint check + [ "$status" -eq 0 ] + + # Looks good. Now we could just rm it, but let's just source it, and let it remove itself, eh? + source "${file}" + + # Make sure it removed itself + [ ! -e "${file}" ] +} + +@test "verify basic usage output with --help" { + load ../optparse + optparse.define short=t long=testing desc="Test_attribute_description" variable="TESTATTRIB" value="true" default="false" + eval set -- --help + file=$(optparse.build) + output=`source <(cat "${file}")`; + [[ "$output" == usage:* && "$output" == *--testing* && "$output" == *Test_attribute_description* ]] +} diff --git a/tests/optparse.run.bats b/tests/optparse.run.bats new file mode 100644 index 0000000..a5a6b82 --- /dev/null +++ b/tests/optparse.run.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats + +############################################################################### +# These test cases use the RUN bats keyword. This is equivalent to running +# the 'run' script right from the shell. The return status is saved as $status, +# while output is under $output and each line is saved in the $lines array. +# +# For optparse, this requires a wrapper script, which just prints our input +# back to us, so we can verify optparse did everything correctly. This method +# is for light / simple tests. For more advanced testing, see the 'load' test +# suite. +# +# See *tests/interface.bash* for more documentation. Currently supported +# arguments (from tests/interface.bash --help) are: +# -i --input: The input file. No default +# -o --output: Output file. Default is default_value +# [default:default_value] +# -a --attrib: Boolean style attribute. [default:false] +# -d --default-value-with-spaces: An argument which has spaces in it's +# default value [default:default value with spaces] +# -s --default-value-with-specials: An argument with a few special +# characters in it. A single quote should be handled ok +# [default:this is ' the !@#$%^&*( ${P\} special values] + +############################################################################### + +# This sets our global mode' nounset should be used instead of print. +export OPTPARSE_TEST_MODE="nounset" + +@test "test bash set -o nounset - fail when accessing undefined variables" { + export OPTPARSE_TEST_MODE="nounset" + run ./tests/interface.bash --input afile -a + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=afile" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=true" ] + [ "${lines[3]}" = "DEFAULT_WITH_SPACES=default value with spaces" ] +} + +@test "run with no arguments" { + run ./tests/interface.bash + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=false" ] + [ "${lines[3]}" = "DEFAULT_WITH_SPACES=default value with spaces" ] +} + +@test "specify short input argument" { + run ./tests/interface.bash -i DEADBEEF + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=false" ] + [ "${lines[3]}" = "DEFAULT_WITH_SPACES=default value with spaces" ] +} + +@test "specify long input argument" { + run ./tests/interface.bash --input DEADBEEF + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "override default argument with shortopt" { + run ./tests/interface.bash --input DEADBEEF -o OVERRIDDEN + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=OVERRIDDEN" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "override default argument with longopt" { + run ./tests/interface.bash --input DEADBEEF --output OVERRIDDEN + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=OVERRIDDEN" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "test boolean value with shortname" { + run ./tests/interface.bash -a + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=true" ] +} + +@test "test boolean value with longname" { + run ./tests/interface.bash --attrib + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=true" ] +} + +@test "use an invalid argument" { + run ./tests/interface.bash --unspecified_argument + [ "$status" -eq 1 ] + [ "${lines[0]}" = "Unrecognized long option: --unspecified_argument" ] +} + +@test "test if -- stops argument processing" { + run ./tests/interface.bash -o one -- -o two + [ "$status" -eq 0 ] +} From 90743d8078742993b45f8dda41c3c145d28915b1 Mon Sep 17 00:00:00 2001 From: Erik Phillips Date: Fri, 3 Jun 2016 14:07:10 -0600 Subject: [PATCH 2/2] Updated unit tests for compatibility with nk412/optparse for pull request. Moved the nounset test to bottom, and removed default value with spaces, which are addressed in seperate pull requests. We still fail 2 tests at the end, but that's normal --- tests/interface.bash | 4 ++-- tests/optparse.run.bats | 23 ++++++++++------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/interface.bash b/tests/interface.bash index 58fa0f2..320c97a 100755 --- a/tests/interface.bash +++ b/tests/interface.bash @@ -44,8 +44,8 @@ mode_print() { optparse.define short=i long=input desc="The input file. No default" required="true" variable=INPUT value="" optparse.define short=o long=output desc="Output file. Default is default_value" variable=OUTPUT default="default_value" optparse.define short=a long=attrib desc="Boolean style attribute." variable="ATTRIB" value="true" default="false" - optparse.define short=d long=default-value-with-spaces desc="An argument which has spaces in it's default value" variable=DEFAULT_WITH_SPACES default="default value with spaces" - optparse.define short=s long=default-value-with-specials desc="An argument with a few special characters in it. A single quote should be handled ok" variable=DEFAULT_WITH_SPECIALS default="this is ' the !@#$%^&*( \${P\} special values" +# optparse.define short=d long=default-value-with-spaces desc="An argument which has spaces in it's default value" variable=DEFAULT_WITH_SPACES default="default value with spaces" +# optparse.define short=s long=default-value-with-specials desc="An argument with a few special characters in it. A single quote should be handled ok" variable=DEFAULT_WITH_SPECIALS default="this is ' the !@#$%^&*( \${P\} special values" source $(optparse.build); echo "INPUT=${INPUT}"; diff --git a/tests/optparse.run.bats b/tests/optparse.run.bats index a5a6b82..a9a2a26 100644 --- a/tests/optparse.run.bats +++ b/tests/optparse.run.bats @@ -25,17 +25,7 @@ ############################################################################### # This sets our global mode' nounset should be used instead of print. -export OPTPARSE_TEST_MODE="nounset" - -@test "test bash set -o nounset - fail when accessing undefined variables" { - export OPTPARSE_TEST_MODE="nounset" - run ./tests/interface.bash --input afile -a - [ "$status" -eq 0 ] - [ "${lines[0]}" = "INPUT=afile" ] - [ "${lines[1]}" = "OUTPUT=default_value" ] - [ "${lines[2]}" = "ATTRIB=true" ] - [ "${lines[3]}" = "DEFAULT_WITH_SPACES=default value with spaces" ] -} +#export OPTPARSE_TEST_MODE="nounset" @test "run with no arguments" { run ./tests/interface.bash @@ -43,7 +33,6 @@ export OPTPARSE_TEST_MODE="nounset" [ "${lines[0]}" = "INPUT=" ] [ "${lines[1]}" = "OUTPUT=default_value" ] [ "${lines[2]}" = "ATTRIB=false" ] - [ "${lines[3]}" = "DEFAULT_WITH_SPACES=default value with spaces" ] } @test "specify short input argument" { @@ -52,7 +41,6 @@ export OPTPARSE_TEST_MODE="nounset" [ "${lines[0]}" = "INPUT=DEADBEEF" ] [ "${lines[1]}" = "OUTPUT=default_value" ] [ "${lines[2]}" = "ATTRIB=false" ] - [ "${lines[3]}" = "DEFAULT_WITH_SPACES=default value with spaces" ] } @test "specify long input argument" { @@ -105,3 +93,12 @@ export OPTPARSE_TEST_MODE="nounset" run ./tests/interface.bash -o one -- -o two [ "$status" -eq 0 ] } + +@test "test bash set -o nounset - fail when accessing undefined variables" { + export OPTPARSE_TEST_MODE="nounset" + run ./tests/interface.bash --input afile -a + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=afile" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=true" ] +}