diff --git a/bash/bats/bats_tap12_to_tap13.awk b/bash/bats/bats_tap12_to_tap13.awk new file mode 100644 index 0000000..f513cea --- /dev/null +++ b/bash/bats/bats_tap12_to_tap13.awk @@ -0,0 +1,76 @@ +#!/usr/bin/awk -f +# +# usage: awk -f bats_tap12_to_tap13.awk /path/to/bash/script > output.tap13 +# + +# BEGIN used for top level declarations for vars that need a reset per top-level match. +BEGIN { + tap13 = "" + sys_out = "" + cmd_output = "" +} + +function append_sysout(sys_out, cmd_output) { + if (sys_out) { + if (cmd_output) { + sys_out = sys_out " output: |\n" cmd_output + } + sys_out = " ---\n" sys_out " ...\n" + tap13 = tap13 sys_out + } + return tap13 +} + +! /^#/ { + tap13 = append_sysout(sys_out, cmd_output) + sys_out = "" + cmd_output = "" + tap13 = tap13 $0 "\n" +} + +/^#/ { + in_sys_out = 1 +} + +in_sys_out { + if (! /^#/) { + in_cmd_out = 0 + in_sys_out = 0 + } + else { + file_name = "" + line_no = "" + status = "" + line = "" + failed_assertion = "" + if (/.*in test file/) { + file_name = $0 + sub(/^.*in test file */, "", file_name) + sub(/,.*$/, "", file_name) + + line_no = $0 + sub(/^.*line */, "", line_no) + sub(/[^0-9]+$/, "", line_no) + sys_out = sys_out " file: " file_name "\n" + sys_out = sys_out " line: " line_no "\n" + } + else if (/^# *status: [0-9]+/) { + status = $0 + gsub(/[^0-9]/, "", status) + sys_out = sys_out " status: " status "\n" + + } + else { + line = $0 + sub(/^# */, " ", line) + cmd_output = cmd_output line "\n" + } + } +} + +END { + tap13 = append_sysout(sys_out, cmd_output) + sys_out = "" + cmd_output = "" + print tap13 +} diff --git a/bash/bats/habitual/git/check_for_changes.bats b/bash/bats/habitual/git/check_for_changes.bats new file mode 100644 index 0000000..c974d17 --- /dev/null +++ b/bash/bats/habitual/git/check_for_changes.bats @@ -0,0 +1,189 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "check_for_changes takes dir as arg" { + # ... setup - starting dir is non-git dir + dir="$TMPDIR/not-a-git-dir" + mkdir -p $dir + use_test_repo_copy + cd $dir # move out of test repo, so check_for_changes moves to user-specified dir + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + echo $output | grep -q "checking for uncommitted changes in $TEST_REPO" +} + +@test "check_for_changes defaults to current dir if no arg" { + # ... setup + use_test_repo_copy + + # ... run + run check_for_changes + print_on_err + + # ... verify + echo $output | grep -q "checking for uncommitted changes in $TEST_REPO" +} + +@test "check_for_changes fails if dir does not exist" { + # ... setup + dir="$TMPDIR/does-not-exist" + + # ... run + run check_for_changes "$dir" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "couldn't cd to $dir" +} + +@test "check_for_changes fails if dir not a git dir" { + # ... setup + dir="$TMPDIR/not-a-git-dir" + mkdir -p $dir + + # ... run + run check_for_changes "$dir" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q 'not a git dir' +} + +@test "check_for_changes passes if no uncommitted changes" { + # ... setup + use_test_repo_copy + + # ... run + run check_for_changes + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "checking for uncommitted changes in $TEST_REPO" + echo $output | grep -q '... none found' +} + +@test "check_for_changes fails if git-tracked file modified (no add)" { + # ... setup + use_test_repo_copy + echo "foo" >> README.md + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "local changes in $TEST_REPO" +} + +@test "check_for_changes fails if git-tracked file modified (git added)" { + # ... setup + use_test_repo_copy + echo "foo" >> README.md + git add README.md + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "local changes in $TEST_REPO" +} + +@test "check_for_changes fails if git-tracked file deleted (no add)" { + # ... setup + use_test_repo_copy + rm README.md + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "local changes in $TEST_REPO" +} + +@test "check_for_changes fails if git-tracked file deleted (git add)" { + # ... setup + use_test_repo_copy + rm README.md + git add --all + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "local changes in $TEST_REPO" +} + +@test "check_for_changes fails if git-tracked file perms changed (no add)" { + # ... setup + use_test_repo_copy + chmod a+x README.md + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "local changes in $TEST_REPO" +} + +@test "check_for_changes fails if git-tracked file perms changed (git add)" { + # ... setup + use_test_repo_copy + chmod a+x README.md + git add --all + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "local changes in $TEST_REPO" +} + +@test "check_for_changes succeeds if new file appears (no add)" { + # ... setup + use_test_repo_copy + echo "foo" > new_file + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "none found" +} + +@test "check_for_changes fails if new file appears (git added)" { + # ... setup + use_test_repo_copy + echo "foo" > new_file + git add new_file + + # ... run + run check_for_changes "$TEST_REPO" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "local changes in $TEST_REPO" +} diff --git a/bash/bats/habitual/git/git.bash b/bash/bats/habitual/git/git.bash new file mode 100644 index 0000000..62c08fd --- /dev/null +++ b/bash/bats/habitual/git/git.bash @@ -0,0 +1,74 @@ +setup() { + . habitual/std.functions || return 1 + . habitual/git.functions || return 1 + + export TMPDIR=$BATS_TMPDIR/$BATS_TEST_NAME + mkdir -p $TMPDIR || true + + export GIT_REPO_URL="https://github.com/opsgang/libs" + + export TMPL_REPO="/var/tmp/opsgang/libs/repo" + if [[ ! -d $TMPL_REPO ]] || [[ ! -r $TMPL_REPO ]]; then + echo >&2 "ERROR: $TMPL_REPO is not readable directory" + return 1 + fi + + export TEST_REPO="$TMPDIR/repo" + + export _GIT_USER="boo" + export _GIT_EMAIL="boo@boo.com" + + export NEW_BRANCH="new-branch" + export NEW_TAG="new-tag" + + # ... set so that git does not use any global .gitconfig + export HOME=$TMPDIR + + cat <$HOME/.gitconfig +[user] + name = $_GIT_USER + email = $_GIT_EMAIL +EOF + + +} + +export_shas() { + HEAD_SHA=$(git log --format='%H' | head -n 1) + SECOND_SHA=$(git log --format='%H' | sed -n '2p') + export HEAD_SHA SECOND_SHA +} + +teardown() { + rm -rf $BATS_TMPDIR/$BATS_TEST_NAME || true +} + +print_on_err() { + local mode="$1" + if [[ "$mode" == "git_vars" ]]; then + echo "GIT_BRANCH:[${GIT_BRANCH:-not set}]" + echo "GIT_TAG:[${GIT_TAG:-not set}]" + echo "GIT_SHA:[${GIT_SHA:-not set}]" + echo "GIT_USER:[${GIT_USER:-not set}]" + echo "GIT_EMAIL:[${GIT_EMAIL:-not set}]" + echo "GIT_ID:[${GIT_ID:-not set}]" + echo "GIT_INFO:[${GIT_INFO:-not set}]" + echo "EXP PATTERN:[$rx]" + echo "status: ${rc:-unknown}" + elif [[ "$mode" =~ ^git_(branch|tag|sha|user|email|id)$ ]]; then + local var="${mode^^}" ; local val="${!var}" + echo "${var}:[${val:-not set}]" + echo "EXP PATTERN:[$rx]" + echo "status: ${rc:-unknown}" + else + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" + fi +} + +use_test_repo_copy() { + cp -r $TMPL_REPO $TMPDIR + cd $TEST_REPO + git reset --hard &>/dev/null || true +} + diff --git a/bash/bats/habitual/git/git_branch.bats b/bash/bats/habitual/git/git_branch.bats new file mode 100644 index 0000000..a53274b --- /dev/null +++ b/bash/bats/habitual/git/git_branch.bats @@ -0,0 +1,56 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_branch fails if run against a non-git dir" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run git_branch + print_on_err + + # ... verify + [[ $status -ne 0 ]] + echo $output | grep -q 'not in a git repo' +} + +@test "git_branch shows branch name even if head commit tagged" { + + # ... setup - create a tag on new branch + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + run git_branch + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $NEW_BRANCH ]] +} + +@test "git_branch shows nothing if tag checked out" { + + # ... setup - tag a new branch, move ahead a commit + # then check out created tag to be sure we are getting + # value of tag not HEAD. + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git checkout $NEW_TAG + + # ... run + run git_branch + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} diff --git a/bash/bats/habitual/git/git_email.bats b/bash/bats/habitual/git/git_email.bats new file mode 100644 index 0000000..0ee2b75 --- /dev/null +++ b/bash/bats/habitual/git/git_email.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_email fails without output if not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + rm $TMPDIR/.gitconfig || true + + # ... run + run git_email + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} + +@test "git_email outputs email.name in git repo if set" { + # ... setup + use_test_repo_copy + + # ... run + run git_email + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $_GIT_EMAIL ]] +} + +@test "git_email fails and outputs nothing in git repo if email unset" { + # ... setup + use_test_repo_copy + rm $TMPDIR/.gitconfig || true + + # ... run + run git_email + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} diff --git a/bash/bats/habitual/git/git_id.bats b/bash/bats/habitual/git/git_id.bats new file mode 100644 index 0000000..711aac4 --- /dev/null +++ b/bash/bats/habitual/git/git_id.bats @@ -0,0 +1,61 @@ +#!/bin/bash +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_id fails without output if not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + rm $TMPDIR/.gitconfig || true + + # ... run + run git_id + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} + +@test "git_id outputs git user and email" { + # ... setup + use_test_repo_copy + + # ... run + run git_id + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "$_GIT_USER $_GIT_EMAIL" ]] +} + +@test "git_id fails if git_user returns empty" { + # ... setup + use_test_repo_copy + git_user() { echo "" ; } + + # ... run + run git_id + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} + +@test "git_id returns space separated strings" { + # ... setup + use_test_repo_copy + + # ... run + read -r a b <<< $(git_id) + print_on_err + + # ... verify + [[ $a == $_GIT_USER ]] + [[ $b == $_GIT_EMAIL ]] +} + diff --git a/bash/bats/habitual/git/git_info_str.bats b/bash/bats/habitual/git/git_info_str.bats new file mode 100644 index 0000000..fc18024 --- /dev/null +++ b/bash/bats/habitual/git/git_info_str.bats @@ -0,0 +1,211 @@ +#!/bin/bash +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_info_str fails if arg is not a dir" { + # ... setup + dir="$TMPDIR/does-not-exist" + + # ... run + run git_info_str "$dir" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "couldn't cd to $dir" +} + +@test "git_info_str fails if arg is not a git dir" { + # ... setup + dir="$TMPDIR/not-a-git-dir" + mkdir -p $dir + + # ... run + run git_info_str "$dir" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "not a git dir" +} + +@test "git_info_str fails with no args if pwd is not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q 'not a git dir' +} + +@test "git_info_str fails silently if git_branch() fails" { + + # ... setup - working dir is non-git dir + use_test_repo_copy + git_branch() { false ; } + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} + +@test "git_info_str prints expected format" { + # ... setup - working dir is non-git dir + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + export_shas + head_sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "repo:$GIT_REPO_URL sha1:$head_sha tag:$NEW_TAG branch:$NEW_BRANCH" ]] +} + +@test "git_info_str defaults to '-no remote-' if no repo found" { + # ... setup - working dir is non-git dir + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + export_shas + head_sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + + git_repo() { echo ""; } + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "repo:-no remote- sha1:$head_sha tag:$NEW_TAG branch:$NEW_BRANCH" ]] + +} + +@test "git_info_str defaults to '-no tag-' if no tag found" { + # ... setup - working dir is non-git dir + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + export_shas + head_sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + + git_tag() { echo ""; } + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "repo:$GIT_REPO_URL sha1:$head_sha tag:-no tag- branch:$NEW_BRANCH" ]] + +} + +@test "git_info_str defaults to '-no branch-' if no branch found" { + # ... setup - working dir is non-git dir + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + export_shas + sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + + git_branch() { echo ""; } + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "repo:$GIT_REPO_URL sha1:$sha tag:$NEW_TAG branch:-no branch-" ]] + +} + +@test "git_info_str on detached HEAD, no tag, no branch" { + # ... setup - working dir is non-git dir + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + echo 'new commit 2' >>README.md + git commit -am "another arbitrary change for test $BATS_TEST_NAME" + + export_shas + git checkout $SECOND_SHA # should result in DETACHED_HEAD + sha="${SECOND_SHA:0:$GIT_SHA_LEN}" + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "repo:$GIT_REPO_URL sha1:$sha tag:-no tag- branch:-no branch-" ]] + +} + +@test "git_info_str on detached HEAD of tag, no branch" { + # ... setup - working dir is non-git dir + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + git tag -a $NEW_TAG -m 'bah' + + git checkout $NEW_TAG # should result in DETACHED_HEAD + + export_shas + sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "repo:$GIT_REPO_URL sha1:$sha tag:$NEW_TAG branch:-no branch-" ]] + +} + +@test "git_info_str if sha is empty string" { + # ... setup - working dir is non-git dir + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + git_sha() { echo ""; } + + # ... run + run git_info_str + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q 'requires a sha1 as second param' + +} diff --git a/bash/bats/habitual/git/git_repo.bats b/bash/bats/habitual/git/git_repo.bats new file mode 100644 index 0000000..008033b --- /dev/null +++ b/bash/bats/habitual/git/git_repo.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_repo succeeds but outputs nothing if pwd is not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run git_repo + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "git_repo succeeds if in a git repo root dir" { + + # ... setup + use_test_repo_copy + + # ... run + run git_repo + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $GIT_REPO_URL ]] +} + +@test "git_repo succeeds if in a git repo sub dir" { + + # ... setup + use_test_repo_copy + mkdir -p foo/bar ; cd foo/bar + + # ... run + run git_repo + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $GIT_REPO_URL ]] +} diff --git a/bash/bats/habitual/git/git_sha.bats b/bash/bats/habitual/git/git_sha.bats new file mode 100644 index 0000000..cc46160 --- /dev/null +++ b/bash/bats/habitual/git/git_sha.bats @@ -0,0 +1,68 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_sha fails with git err if not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run git_sha + print_on_err + + # ... verify + [[ $status -ne 0 ]] + [[ $output =~ "fatal: not a git repository" ]] +} + +@test "git_sha outputs current commit sha on non-detached head" { + # ... setup + use_test_repo_copy + git checkout -b $NEW_BRANCH &>/dev/null + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + export_shas + + # ... run + run git_sha + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $HEAD_SHA =~ ^$output ]] +} + +@test "git_sha outputs current commit sha even on detached head" { + # ... setup + use_test_repo_copy + export_shas + git checkout $SECOND_SHA &>/dev/null + + # ... run + run git_sha + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $SECOND_SHA =~ ^$output ]] +} + +@test "git_sha honours GIT_SHA_LEN" { + # ... setup + use_test_repo_copy + MY_GIT_SHA_LEN=6 + export GIT_SHA_LEN=$MY_GIT_SHA_LEN + export_shas + + # ... run + run git_sha + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $HEAD_SHA =~ ^$output ]] + [[ ${#output} -eq $MY_GIT_SHA_LEN ]] +} diff --git a/bash/bats/habitual/git/git_tag.bats b/bash/bats/habitual/git/git_tag.bats new file mode 100644 index 0000000..7648aa5 --- /dev/null +++ b/bash/bats/habitual/git/git_tag.bats @@ -0,0 +1,356 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_tag succeeds, but no output if not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "git_tag outputs annotated tag name at HEAD" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git tag -a $NEW_TAG -m 'bah' + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $NEW_TAG ]] +} + +@test "git_tag outputs nothing if no tag on HEAD commit" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "git_tag with GIT_SORT=semver outputs nothing if no tag on HEAD commit" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + export GIT_SORT=semver + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "git_tag with GIT_SORT=taggerdate outputs nothing if no tag on HEAD commit" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + export GIT_SORT=taggerdate + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "git_tag outputs lightweight tag at HEAD" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git tag $NEW_TAG + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $NEW_TAG ]] +} + +@test "git_tag defaults to reverse alphanumeric sort order when multiple lightweight" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git tag "${NEW_TAG}-top" + git tag "${NEW_TAG}-001" + git tag "${NEW_TAG}-stetson" + git tag "${NEW_TAG}-apple" + git tag "${NEW_TAG}-fedora" + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $NEW_TAG-top ]] +} + +@test "git_tag defaults to reverse alphanumeric sort order when multiple annotated" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git tag -a "${NEW_TAG}-top" -m 'bah' + git tag -a "${NEW_TAG}-apple" -m 'bah' + git tag -a "${NEW_TAG}-001" -m 'bah' + git tag -a "${NEW_TAG}-stetson" -m 'bah' + git tag -a "${NEW_TAG}-fedora" -m 'bah' + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $NEW_TAG-top ]] +} + +@test "git_tag default lexical sort considers annotated and lightweight #1" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git tag -a "${NEW_TAG}-top" -m 'bah' + git tag -a "${NEW_TAG}-apple" -m 'bah' + git tag -a "${NEW_TAG}-001" -m 'bah' + git tag "${NEW_TAG}-umbrella" # lexically highest, but not annotated + git tag -a "${NEW_TAG}-fedora" -m 'bah' + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $NEW_TAG-umbrella ]] +} + +@test "git_tag default lexical sort considers annotated and lightweight #2" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git tag -a "${NEW_TAG}-top" -m 'bah' + git tag -a "${NEW_TAG}-apple" -m 'bah' + git tag "${NEW_TAG}-toast" + git tag "${NEW_TAG}-tool" + git tag -a "${NEW_TAG}-fedora" -m 'bah' + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $NEW_TAG-top ]] +} + +@test "git_tag semver sort - all tags are annotated" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + for tag in 10.0.2 12.0.0 0.0.1 1.1.1 ; do git tag -a $tag -m 'bah' ; done + export GIT_SORT=semver + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == 12.0.0 ]] +} + +@test "git_tag semver sort - all tags are lightweight" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + for tag in 10.0.2 12.0.0 0.0.1 1.1.1 ; do git tag $tag ; done + export GIT_SORT=semver + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == 12.0.0 ]] +} + +@test "git_tag semver sort - same if lightweight or annotated #1" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + lightweight="10.0.2 12.0.0" + annotated="0.0.1 1.1.1" + + for tag in $annotated ; do git tag -a $tag -m 'foo' ; done + for tag in $lightweight ; do git tag $tag ; done + export GIT_SORT=semver + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == 12.0.0 ]] +} + +@test "git_tag semver sort - same if lightweight or annotated #2" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + annotated="10.0.2 12.0.0" + lightweight="0.0.1 1.1.1" + + for tag in $annotated ; do git tag -a $tag -m 'foo' ; done + for tag in $lightweight ; do git tag $tag ; done + export GIT_SORT=semver + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == 12.0.0 ]] +} + +@test "git_tag semver sort - same if non-semver annotated tag" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + annotated="10.0.2 12.0.0 alpha 100alpha.0.0" + lightweight="0.0.1 1.1.1" + + for tag in $annotated ; do git tag -a $tag -m 'foo' ; done + for tag in $lightweight ; do git tag $tag ; done + export GIT_SORT=semver + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == 12.0.0 ]] +} + +@test "git_tag semver sort - same if non-semver lightweight tag" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + annotated="10.0.2 12.0.0 " + lightweight="0.0.1 1.1.1 alpha 100alpha.0.0" + + for tag in $annotated ; do git tag -a $tag -m 'foo' ; done + for tag in $lightweight ; do git tag $tag ; done + export GIT_SORT=semver + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == 12.0.0 ]] +} + +@test "git_tag with GIT_SORT=taggerdate lexical sort if only lightweight tags" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + for tag in a c b ; do git tag $tag ; sleep 1.1 ; done + + export GIT_SORT=taggerdate + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "c" ]] +} + +@test "git_tag with GIT_SORT=taggerdate annotated tags preferred" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + git tag a ; sleep 1.1 ; git tag -a b -m foo ; sleep 1.1 ; git tag c + + export GIT_SORT=taggerdate + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "b" ]] +} + +@test "git_tag with GIT_SORT=taggerdate using annotated tags" { + # ... setup + use_test_repo_copy + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + + for tag in 10.0.2 12.0.0 11.0.1 ; do git tag -a $tag -m 'foo'; sleep 1.1 ; done + export GIT_SORT=taggerdate + + # ... run + run git_tag + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "11.0.1" ]] +} + diff --git a/bash/bats/habitual/git/git_user.bats b/bash/bats/habitual/git/git_user.bats new file mode 100644 index 0000000..269b7bc --- /dev/null +++ b/bash/bats/habitual/git/git_user.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_user fails without output if not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + rm $TMPDIR/.gitconfig || true + + # ... run + run git_user + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} + +@test "git_user outputs user.name in git repo if set" { + # ... setup + use_test_repo_copy + + # ... run + run git_user + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == $_GIT_USER ]] +} + +@test "git_user fails and outputs nothing in git repo if user unset" { + # ... setup + use_test_repo_copy + rm $TMPDIR/.gitconfig || true + + # ... run + run git_user + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} diff --git a/bash/bats/habitual/git/git_vars.bats b/bash/bats/habitual/git/git_vars.bats new file mode 100644 index 0000000..3ad86b0 --- /dev/null +++ b/bash/bats/habitual/git/git_vars.bats @@ -0,0 +1,180 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "git_vars fails if pwd is not a git repo" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run git_vars + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q 'is not inside a git repo' +} + +@test "git_vars fails if git_branch() fails" { + + # ... setup + use_test_repo_copy + git_branch() { return 1; } + + # ... run + run git_vars + print_on_err + + # ... verify + [[ $status -eq 1 ]] +} + +# ... create known branch and tag state +# Can't use run func as that creates a subshell so +# exported GIT_INFO would be lost to outer shell. +# +# We can't know the sha1 with out basically adding a test +# function that is ultimately the same as one of the functions +# under test, so we will leave that testing to the git_sha() tests. +@test "git_vars exports a GIT_INFO str" { + + # ... setup + use_test_repo_copy + unset GIT_INFO + rx="repo:$GIT_REPO_URL sha1:\w+ tag:$NEW_TAG branch:$NEW_BRANCH" + + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + git_vars # exports GIT_INFO to this shell. + rc=$? + print_on_err git_vars + + # ... verify + [[ $rc -eq 0 ]] + echo "$GIT_INFO" | grep -Pq "$rx" +} + +@test "git_vars exports a GIT_BRANCH" { + + # ... setup + use_test_repo_copy + unset GIT_BRANCH + rx="^$NEW_BRANCH$" + + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + git_vars # exports GIT_INFO to this shell. + rc=$? + print_on_err git_vars + + # ... verify + [[ $rc -eq 0 ]] + echo "$GIT_BRANCH" | grep -q "$rx" +} + +@test "git_vars exports a GIT_TAG" { + + # ... setup + use_test_repo_copy + unset GIT_TAG + rx="^$NEW_TAG$" + + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + git_vars # exports GIT_INFO to this shell. + rc=$? + print_on_err git_vars + + # ... verify + [[ $rc -eq 0 ]] + echo "$GIT_TAG" | grep -q "$rx" +} + +@test "git_vars exports a GIT_SHA" { + + # ... setup + use_test_repo_copy + unset GIT_SHA + rx="^\w+$" + + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + git_vars # exports GIT_INFO to this shell. + rc=$? + print_on_err git_vars + + # ... verify + [[ $rc -eq 0 ]] + echo "$GIT_SHA" | grep -Pq "$rx" +} + +@test "git_vars exports a GIT_USER" { + + # ... setup + use_test_repo_copy + unset GIT_USER + rx="^$_GIT_USER$" + + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + git_vars # exports GIT_INFO to this shell. + rc=$? + print_on_err git_vars + + # ... verify + [[ $rc -eq 0 ]] + echo "$GIT_USER" | grep -Pq "$rx" +} + +@test "git_vars exports a GIT_EMAIL" { + + # ... setup + use_test_repo_copy + unset GIT_EMAIL + rx="^$_GIT_EMAIL$" + + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + git_vars # exports GIT_INFO to this shell. + rc=$? + print_on_err git_vars + + # ... verify + [[ $rc -eq 0 ]] + echo "$GIT_EMAIL" | grep -Pq "$rx" +} + +@test "git_vars exports a GIT_ID" { + + # ... setup + use_test_repo_copy + unset GIT_ID + rx="^$_GIT_USER $_GIT_EMAIL$" + + git checkout -b $NEW_BRANCH &>/dev/null + git tag -a "$NEW_TAG" -m 'bah' + + # ... run + git_vars # exports GIT_INFO to this shell. + rc=$? + print_on_err git_vars + + # ... verify + [[ $rc -eq 0 ]] + echo "$GIT_ID" | grep -Pq "$rx" +} diff --git a/bash/bats/habitual/git/in_git_clone.bats b/bash/bats/habitual/git/in_git_clone.bats new file mode 100644 index 0000000..8b00259 --- /dev/null +++ b/bash/bats/habitual/git/in_git_clone.bats @@ -0,0 +1,75 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "in_git_clone fails with no arg if current dir is not a git dir" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run in_git_clone + print_on_err + + # ... verify + [[ $status -eq 1 ]] +} + +@test "in_git_clone succeeds with no arg if current dir *is* a git dir" { + + # ... setup + use_test_repo_copy + + # ... run + run in_git_clone + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "in_git_clone fails if empty str used and pwd is not a git dir" { + + # ... setup - working dir is non-git dir + mkdir -p $TMPDIR/foo ; cd $TMPDIR/foo + + # ... run + run in_git_clone "" + print_on_err + + # ... verify + [[ $status -eq 1 ]] +} + +@test "in_git_clone succeeds if empty str used and pwd *is* a git dir" { + + # ... setup + use_test_repo_copy + + # ... run + run in_git_clone "" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +# ... can't use bats' run func when proving in_git_clone leaves user +# in the starting dir, because that creates a subshell. +# A subshell will return user to current dir anyway after completion. +@test "in_git_clone does not leave user in different pwd" { + # ... setup + current_dir=$(pwd) ; + print_on_err + use_test_repo_copy ; cd $current_dir + + # ... run + in_git_clone $TMPDIR/repo + + # ... verify + [[ $? -eq 0 ]] + [[ "$(pwd)" == "$current_dir" ]] +} + diff --git a/bash/bats/habitual/git/no_unpushed_changes.bats b/bash/bats/habitual/git/no_unpushed_changes.bats new file mode 100644 index 0000000..cc7ea30 --- /dev/null +++ b/bash/bats/habitual/git/no_unpushed_changes.bats @@ -0,0 +1,18 @@ +#!/bin/bash +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "no_unpushed_changes skips checks if DEVMODE" { + # ... setup - working dir is non-git dir + export DEVMODE=true + + # ... run + run no_unpushed_changes + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output =~ skipping\ git\ checks ]] +} diff --git a/bash/bats/habitual/git/sha_in_origin.bats b/bash/bats/habitual/git/sha_in_origin.bats new file mode 100644 index 0000000..090736f --- /dev/null +++ b/bash/bats/habitual/git/sha_in_origin.bats @@ -0,0 +1,159 @@ +#!/bin/bash +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# + +load git + +@test "sha_in_origin takes sha as user passed arg" { + # ... setup + sha=made-up-sha + + # ... run + run sha_in_origin $sha + print_on_err + + # ... verify + echo $output | grep -q "checking git sha $sha exists in origin" +} + +@test "sha_in_origin takes GIT_SHA if set and arg not passed" { + # ... setup + sha='GIT_SHA-set-in-env-no-arg' + export GIT_SHA="$sha" + + # ... run + run sha_in_origin + print_on_err + + # ... verify + echo $output | grep -q "checking git sha $sha exists in origin" +} + +@test "sha_in_origin takes current sha val if GIT_SHA not set and arg not passed" { + # ... setup + use_test_repo_copy + export_shas + sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + + # ... run + run sha_in_origin + print_on_err + + # ... verify + echo $output | grep -q "checking git sha $sha exists in origin" +} + +@test "sha_in_origin prefers user-passed sha" { + # ... setup + use_test_repo_copy + export GIT_SHA="should-not-be-used" + sha="should-be-preferred" + + # ... run + run sha_in_origin "$sha" + print_on_err + + # ... verify + echo $output | grep -q "checking git sha $sha exists in origin" +} + +@test "sha_in_origin prefers GIT_SHA if no arg passed" { + # ... setup + use_test_repo_copy + sha="should-be-preferred" + export GIT_SHA="$sha" + + # ... run + run sha_in_origin + print_on_err + + # ... verify + echo $output | grep -q "checking git sha $sha exists in origin" +} + +@test "sha_in_origin fails if no arg and GIT_SHA not set and not in git repo" { + # ... setup + dir="$TMPDIR/not-a-git-dir" + mkdir -p $dir + cd $dir + + # ... run + run sha_in_origin + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q 'no git_sha passed as param, or $GIT_SHA or sha' +} + +@test "sha_in_origin uses GIT_SHA if user passes empty string" { + # ... setup + use_test_repo_copy + sha="" + export GIT_SHA="will-use-this" + + # ... run + run sha_in_origin "$sha" + print_on_err + + # ... verify + echo $output | grep -q "checking git sha $GIT_SHA exists in origin" +} + +@test "sha_in_origin uses current sha if GIT_SHA and user-passed arg are empty strs" { + # ... setup + use_test_repo_copy + export_shas + real_sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + sha="" + export GIT_SHA="" + + # ... run + run sha_in_origin "$sha" + print_on_err + + # ... verify + echo $output | grep -q "checking git sha $real_sha exists in origin" +} + +@test "sha_in_origin fails if sha not in origin" { + # ... setup + use_test_repo_copy + + git checkout -b $NEW_BRANCH &>/dev/null + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + export_shas + + sha="${HEAD_SHA:0:$GIT_SHA_LEN}" + + # ... run + run sha_in_origin + print_on_err + + # ... verify + [[ $status -eq 1 ]] + echo $output | grep -q "($sha) does not exist on origin" +} + +@test "sha_in_origin succeeds if detached head but in origin" { + # ... setup + use_test_repo_copy + + git checkout -b $NEW_BRANCH &>/dev/null + echo 'new commit' >>README.md + git commit -am "arbitrary change for test $BATS_TEST_NAME" + export_shas + + git checkout $SECOND_SHA &>/dev/null # detached head + sha="${SECOND_SHA:0:$GIT_SHA_LEN}" + + # ... run + run sha_in_origin + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "checking git sha $sha exists in origin" + echo $output | grep -q 'all looking copacetic' +} diff --git a/bash/bats/habitual/std/__stacktrace.bats b/bash/bats/habitual/std/__stacktrace.bats new file mode 100644 index 0000000..428e01b --- /dev/null +++ b/bash/bats/habitual/std/__stacktrace.bats @@ -0,0 +1,57 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + if [[ ! -z "$exp_output" ]]; then + echo " EXP OUTPUT--|$exp_output|--EXP OUTPUT" + fi + echo "status: $status" +} + +setup() { + export LIB=$(realpath habitual/std.functions) + export TMPDIR=$BATS_TMPDIR/$BATS_TEST_NAME + + mkdir -p $TMPDIR || true + + . $LIB || return 1 +} + +teardown() { + rm -rf $TMPDIR || true +} + +mkscript() { + local script="$1" + local f=$(mktemp) + cat < $f +#!/bin/bash -e +$script +EOF + + chmod a+x $f + echo "$f" +} + +@test "__stacktrace output meets expected format" { + # ... setup + f3=$(mkscript "f3_main() { . $LIB ; FROM_STACKFRAME=0 ; __stacktrace && true ; } ;") + f2=$(mkscript "f2_main() { . $f3 ; f3_main ; } ; ") + f1=$(mkscript "f1_main() { . $f2 ; f2_main ; } ; f1_main") + + # exp_output will also contain the main() from the temp bats script + # but this is enough to verify the format we expect + exp_output="f3_main() (file: $f3, line: 2)\\n" + exp_output="$exp_output f2_main() (file: $f2, line: 2)\\n" + exp_output="$exp_output f1_main() (file: $f1, line: 2)\\n" + + # ... run + run $f1 + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "$exp_output"* ]] +} + diff --git a/bash/bats/habitual/std/check_var_defined.bats b/bash/bats/habitual/std/check_var_defined.bats new file mode 100644 index 0000000..5422ef1 --- /dev/null +++ b/bash/bats/habitual/std/check_var_defined.bats @@ -0,0 +1,36 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 +} + +@test "check_var_defined success if var has a val" { + my_var="some value" + + run check_var_defined my_var + print_on_err + [[ $status -eq 0 ]] +} + +@test "check_var_defined fails if empty" { + my_var="" + + run check_var_defined my_var + print_on_err + [[ $status -ne 0 ]] +} + +@test "check_var_defined fails if var unser" { + my_var="some value" + unset my_var + + run check_var_defined my_var + print_on_err + [[ $status -ne 0 ]] +} diff --git a/bash/bats/habitual/std/d.bats b/bash/bats/habitual/std/d.bats new file mode 100644 index 0000000..6550ed2 --- /dev/null +++ b/bash/bats/habitual/std/d.bats @@ -0,0 +1,126 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 + unset DEBUG + unset QUIET +} + +@test "d has no output if DEBUG undefined" { + # ... setup + unset DEBUG + + # ... run + run d "this str should not be in output" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "" ]] +} + +@test "d has no output if DEBUG empty str" { + # ... setup + export DEBUG="" + + # ... run + run d "this str should not be in output" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "" ]] +} + +@test "d has output if DEBUG set to non-empty value" { + # ... setup + export DEBUG=true + + # ... run + run d "this str should be output" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "this str should be output" +} + +@test "d has output even if DEBUG set to whitespace" { + # ... setup + export DEBUG=" " + + # ... run + run d "this str should be output" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "this str should be output" +} + +@test "d has no output if QUIET set even if DEBUG is set" { + # ... setup + export DEBUG="true" QUIET="true" + + # ... run + run d "this str should not be output" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "" ]] +} + +@test "d will render actual newlines when passed multiple lines" { + # ... setup + export DEBUG=true + msg="before 1st new line. + before 2nd new line. + before 3nd new line." + + # ... run + run d "$msg" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $(echo "$output" | wc -l) -eq 3 ]] + +} + +@test "d will render slash-n as actual newlines" { + # ... setup + export DEBUG=true + msg="before 1st new line.\nbefore 2nd new line.\nbefore 3nd new line." + + # ... run + run d "$msg" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $(echo "$output" | wc -l) -eq 3 ]] + +} + +@test "d will render mix of actual newlines and slash-n as actual newlines" { + # ... setup + export DEBUG=true + msg="before 1st new line.\nbefore 2nd new line. + before 3nd new line." + + # ... run + run d "$msg" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $(echo "$output" | wc -l) -eq 3 ]] + +} diff --git a/bash/bats/habitual/std/envsubst_tokens_list.bats b/bash/bats/habitual/std/envsubst_tokens_list.bats new file mode 100644 index 0000000..d7191f5 --- /dev/null +++ b/bash/bats/habitual/std/envsubst_tokens_list.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 +} + +@test "envsubst_tokens_list produces string of tokens as expected" { + # ... setup + expected_str='${apple} ${banana} ${carrot}' + + # ... run + run envsubst_tokens_list 'apple banana carrot' + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$expected_str" ]] +} + +@test "envsubst_tokens_list produces empty str if no arg" { + # ... run + run envsubst_tokens_list '' + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "" ]] +} + +@test "envsubst_tokens_list outputs empty and err if invalid var names" { + # ... setup + expected_invalid='$apple @banana 1date' + + # ... run + run envsubst_tokens_list '$apple @banana _carrot 1date' + print_on_err + + # ... verify + [[ $status -ne 0 ]] + echo $output | grep -q "following tokens are invalid.*$expected_invalid" +} diff --git a/bash/bats/habitual/std/export_build_url.bats b/bash/bats/habitual/std/export_build_url.bats new file mode 100644 index 0000000..77b9ce6 --- /dev/null +++ b/bash/bats/habitual/std/export_build_url.bats @@ -0,0 +1,87 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + unset BUILD_URL + . habitual/std.functions || return 1 +} + +@test "export_build_url fails if no build url can be determined" { + # ... run + run export_build_url + print_on_err + + # ... verify + [[ $status -eq 1 ]] +} + +@test "export_build_url honours BUILD_URL" { + # ... setup + DEBUG=true + BUILD_URL="https://example.com/foo/bar/1" + url="$BUILD_URL" + + # ... run + run export_build_url + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "BUILD_URL.*$url" +} + +@test "export_build_url honours CIRCLE_BUILD_URL" { + # ... setup + DEBUG=true + CIRCLE_BUILD_URL="should use this value" + BUILD_URL="should not be used" + + url="$CIRCLE_BUILD_URL" + + # ... run + run export_build_url + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "BUILD_URL.*$url" +} + +@test "export_build_url honours TRAVIS_JOB_WEB_BUILD_URL" { + # ... setup + DEBUG=true + TRAVIS_JOB_WEB_URL="should use travis value" + BUILD_URL="should not be used" + + url="$TRAVIS_JOB_WEB_URL" + + # ... run + run export_build_url + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "BUILD_URL.*$url" +} + +@test "export_build_url honours CI_BUILD_URL" { + # ... setup + DEBUG=true + CI_BUILD_URL="should use this codeship-compatible val" + BUILD_URL="should not be used" + + url="$CI_BUILD_URL" + + # ... run + run export_build_url + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q "BUILD_URL.*$url" +} diff --git a/bash/bats/habitual/std/misc_funcs_tests_to_ensure_existence.bats b/bash/bats/habitual/std/misc_funcs_tests_to_ensure_existence.bats new file mode 100644 index 0000000..1cbc149 --- /dev/null +++ b/bash/bats/habitual/std/misc_funcs_tests_to_ensure_existence.bats @@ -0,0 +1,32 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +# For functions that don't need or can't meaningfully have tests. +# We still want to ensure they exist in the lib and that if we +# deprecate or remove one we consciously amend the test accordingly. +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 +} + +@test "safe_chars_def_list exists" { + # ... run + run safe_chars_def_list + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "random_str exists" { + run random_str + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} diff --git a/bash/bats/habitual/std/render_tmpl.bats b/bash/bats/habitual/std/render_tmpl.bats new file mode 100644 index 0000000..15908b8 --- /dev/null +++ b/bash/bats/habitual/std/render_tmpl.bats @@ -0,0 +1,228 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 + + export TMPDIR=$BATS_TMPDIR/$BATS_TEST_NAME + mkdir -p $TMPDIR || true + + export FIXTURES="t/habitual/fixtures" + export SL_TMPL="$FIXTURES/singleline.tmpl" + export ML_TMPL="$FIXTURES/multiline.tmpl" + export EXPECTED_ML_RESULT="$FIXTURES/multiline.result" +} + +teardown() { + rm -rf $BATS_TMPDIR/$BATS_TEST_NAME || true +} + +@test "std::render_tmpl fails if path to tmpl not passed" { + + # ... run + run std::render_tmpl + print_on_err + + # ... verify + [[ $status -ne 0 ]] + echo "$output" | grep -q 'expects either /path/to/file as arg or as env var' +} + +@test "std::render_tmpl with a single line template, all vars with vals" { + # ... setup + number=2 fruit=apple + + # ... run + run std::render_tmpl $SL_TMPL + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo "$output" | grep -q '^I eat 2 apples.$' +} + +@test "std::render_tmpl with a single line template, all vars no vals" { + # ... run + run std::render_tmpl $SL_TMPL + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo "$output" | grep -q '^I eat s.$' +} + +@test "std::render_tmpl with a single line template, file_tmpl env var set" { + # ... setup + number=2 fruit=apple file_tmpl=$SL_TMPL + + # ... run + run std::render_tmpl + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo "$output" | grep -q '^I eat 2 apples.$' +} + +@test "std::render_tmpl prefers arg to file_tmpl env_var" { + # ... setup + number=2 fruit=apple file_tmpl=/path/does/not/exist + + # ... run + run std::render_tmpl $SL_TMPL + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo "$output" | grep -q '^I eat 2 apples.$' +} + +@test "std::render_tmpl fails if tmpl file unreadable" { + # ... setup + f=$(mktemp) + cp $SL_TMPL $f + chmod 0333 $f + + # ... run + run std::render_tmpl $f + print_on_err + + # ... verify + [[ $status -ne 0 ]] + echo "$output" | grep -q "$f is not readable" +} + +@test "std::render_tmpl renders simple multiline tmpl" { + # ... setup + outfile=$(mktemp) + local file_tmpl="$ML_TMPL" prenombre="Foo" apellido="Bar" + + # ... run + std::render_tmpl > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile $EXPECTED_ML_RESULT +} + +@test "std::render_tmpl renders tmpl as empty str" { + # ... setup + infile=$(mktemp) ; outfile=$(mktemp) + echo '$not_defined'>$infile + + # ... run + std::render_tmpl $infile > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile <(echo "") +} + +@test "std::render_tmpl multiline tmpl with some undefined vars" { + # ... setup + file_tmpl=$ML_TMPL apellido="Bar" + outfile=$(mktemp) + + # ... run + std::render_tmpl > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile $FIXTURES/var_not_defined.result +} + +@test "std::render_tmpl backslashed vars should not be interpolated during render" { + # ... setup + file_tmpl="$FIXTURES/escaped_vars.tmpl" + var="\$var is escaped in tmpl so not interpolated" + outfile=$(mktemp) + + # ... run + std::render_tmpl > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile $FIXTURES/escaped_vars.result +} + +@test "std::render_tmpl shell code in tmpl not executed by default" { + # ... setup + file_tmpl="$FIXTURES/breakout.tmpl" + outfile=$(mktemp) + + # ... run + std::render_tmpl > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile $FIXTURES/breakout_not_allowed.result +} + +@test "std::render_tmpl shell code in tmpl executed if allow_code is set" { + # ... setup + file_tmpl="$FIXTURES/breakout.tmpl" + allow_code=true + outfile=$(mktemp) + + # ... run + std::render_tmpl > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile $FIXTURES/breakout_allowed.result +} + +@test "std::render_tmpl shell code in tmpl escaped if not already escaped" { + # ... setup + file_tmpl="$FIXTURES/some_backslashed_breakouts.tmpl" + outfile=$(mktemp) + + # ... run + std::render_tmpl > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile $FIXTURES/some_backslashed_breakouts.result +} + +@test "std::render_tmpl stops multiple backslashes to force shell code execution" { + # ... setup + file_tmpl="$FIXTURES/multiple_backslashes_for_breakouts.tmpl" + outfile=$(mktemp) + + # ... run + std::render_tmpl > $outfile + status=$? + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + diff $outfile $FIXTURES/some_backslashed_breakouts.result +} diff --git a/bash/bats/habitual/std/required_vars.bats b/bash/bats/habitual/std/required_vars.bats new file mode 100644 index 0000000..6116012 --- /dev/null +++ b/bash/bats/habitual/std/required_vars.bats @@ -0,0 +1,36 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 +} + +@test "required_vars success if all vars exist" { + # ... setup + local my_var1=apple my_var2=banana my_var3=carrot + + # ... run + run required_vars "my_var1 my_var2 my_var3" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "required_vars fails if any var missing" { + # ... setup + local my_var1=apple my_var2=banana + + # ... run + run required_vars "my_var1 my_var2 my_var3" + print_on_err + + # ... verify + [[ $status -ne 0 ]] + echo $output | grep -q 'following vars must be set.*$my_var3' +} diff --git a/bash/bats/habitual/std/run_if_exists.bats b/bash/bats/habitual/std/run_if_exists.bats new file mode 100644 index 0000000..8d88f9e --- /dev/null +++ b/bash/bats/habitual/std/run_if_exists.bats @@ -0,0 +1,86 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 +} + +@test "run_if_exists run func with no args" { + local foo="" + # ... setup + foo() { echo "foo bar $@"; } + + # ... run + run std::run_if_exists "foo" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q '^foo bar *$' +} + +@test "run_if_exists run func with args" { + local foo="" + # ... setup + foo() { echo "foo bar $@"; } + + # ... run + run std::run_if_exists "foo" "arg1" "arg2" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q '^foo bar arg1 arg2 *$' +} + +@test "run_if_exists fails if func name not passed" { + run std::run_if_exists + [[ $status -ne 0 ]] + echo "$output" | grep -q 'expects function name as 1st arg' +} + +@test "run_if_exists success but runs nothing if func does not exist" { + # ... setup + DEBUG=true + + # ... run + run std::run_if_exists "foo" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo "$output" | grep -q "function 'foo()' not found" +} + +@test "run_if_exists success but runs nothing if invalid function name" { + # ... setup + DEBUG=true + + # ... run + run std::run_if_exists "; foo" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo "$output" | grep -q "function '; foo()' not found" +} + +@test "run_if_exists returns the exit code of the function" { + local foo="" + # ... setup + foo() { echo "foo bar $@"; return 212; } + + # ... run + run std::run_if_exists "foo" "arg1" "arg2" + print_on_err + + # ... verify + [[ $status -eq 212 ]] + echo $output | grep -q '^foo bar arg1 arg2 *$' +} + diff --git a/bash/bats/habitual/std/semver_a_ge_b.bats b/bash/bats/habitual/std/semver_a_ge_b.bats new file mode 100644 index 0000000..3b1779a --- /dev/null +++ b/bash/bats/habitual/std/semver_a_ge_b.bats @@ -0,0 +1,315 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 +} + +@test "semver_a_ge_b fails if a is not a semver" { + # ... setup + a="foobar" + b="1.0.0" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 2 ]] + echo $output | grep -q 'expects 2 semver strs as params' +} + +@test "semver_a_ge_b fails if b is not a semver" { + # ... setup + a="1.0.0" + b="invalid_semver-1.0.0" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 2 ]] + echo $output | grep -q 'expects 2 semver strs as params' +} + +@test "semver_a_ge_b fails with no args " { + # ... run + run semver_a_ge_b + print_on_err + + # ... verify + [[ $status -eq 2 ]] + echo $output | grep -q 'expects 2 semver strs as params' +} + +@test "semver_a_ge_b fails with only 1 arg" { + # ... run + run semver_a_ge_b "1.0.0" + print_on_err + + # ... verify + [[ $status -eq 2 ]] + echo $output | grep -q 'expects 2 semver strs as params' +} + +@test "semver_a_ge_b succeeds if a > b #1" { + # ... setup + a="1.0.0" + b="0.11.0" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds if a > b #2" { + # ... setup + a="11.0.0" + b="9.15.0" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds if a == b" { + # ... setup + a="3.20.100" + b="$a" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds even if a has leading 'v'" { + # ... setup + a="v3.20.100" + b="1.2.30" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds even if b has leading 'v'" { + # ... setup + a="3.20.100" + b="v1.2.30" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds even if a has leading 'V'" { + # ... setup + a="V3.20.100" + b="1.2.30" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds even if b has leading 'V'" { + # ... setup + a="3.20.100" + b="V1.2.30" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds even if a has 'v' and b has 'V'" { + # ... setup + a="v3.20.100" + b="V1.2.30" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds even if a has 'V' and b has 'v'" { + # ... setup + a="V3.20.100" + b="v1.2.30" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b succeeds if a is prerelease but newer version than b" { + # ... setup + a="1.0.1-not-ready-for-use" + b="1.0.0" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] +} + +@test "semver_a_ge_b returns false if a is prerelease but same version as b" { + # ... setup + a="1.0.0-not-ready-for-use" + b="1.0.0" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b returns false if a is prerelease, same version fewer subparts" { + # ... setup + a="1.0.0-not-ready-for-use.but.wait.a.bit" + b="1.0.0-not-ready-for-use.but.wait.a.bit.longer" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b returns true if a is prerelease, same version more subparts" { + # ... setup + a="1.0.0-not-ready-for-use.but.wait.a.bit.longer" + b="1.0.0-not-ready-for-use.but.wait.a.bit" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b returns true if a is prerelease, higher precedence subparts" { + # ... setup + a="1.0.0-not-ready-for-use.beta" + b="1.0.0-not-ready-for-use.alpha" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b ignores build metadata for precedence #1" { + # ... setup + a="1.0.0+alpha" + b="1.0.0+beta.but.build.metadata.should.be.ignored" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b ignores build metadata for precedence #2" { + # ... setup + a="1.0.0-a+alpha" + b="1.0.0-a+beta" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b ascii prerelease beats numeric prerelease" { + # ... setup + a="1.0.0-a" + b="1.0.0-100.10.1" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b numeric prereleases compared as numbers" { + # ... setup + a="1.0.0-1.10.2" + b="1.0.0-1.2.10" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ $output == "" ]] +} + +@test "semver_a_ge_b numeric prereleases compared as numbers unless a leading 0" { + # ... setup + a="1.0.0-1.020.2" # not numeric sort as leading zero in '020' + b="1.0.0-1.2.10" + + # ... run + run semver_a_ge_b "$a" "$b" + print_on_err + + # ... verify + [[ $status -eq 1 ]] + [[ $output == "" ]] +} diff --git a/bash/bats/habitual/std/set_log_prefix.bats b/bash/bats/habitual/std/set_log_prefix.bats new file mode 100644 index 0000000..f8e95b5 --- /dev/null +++ b/bash/bats/habitual/std/set_log_prefix.bats @@ -0,0 +1,132 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + export LIB=habitual/std.functions + export TMPDIR=$BATS_TMPDIR/$BATS_TEST_NAME + export FROM_STACKFRAME=0 + + mkdir -p $TMPDIR || true + + . $LIB || return 1 +} + +teardown() { + rm -rf $TMPDIR || true +} + +mkscript() { + local script="$1" + local f="" + f=$(mktemp) + cat < $f +#!/bin/bash -e +$script +EOF + + chmod a+x $f + echo "$f" +} + +@test "set_log_prefix run from script - global" { + local f="" ; f=$(mkscript ". $LIB ; set_log_prefix") + run $f + print_on_err + + [[ "$output" == "$(basename $f):main()" ]] +} + +@test "set_log_prefix run from script - function" { + local f="" ; f=$(mkscript ". $LIB ; foo() { set_log_prefix ; } ; foo") + run $f + print_on_err + + [[ "$output" == "$(basename $f):foo()" ]] +} + +@test "set_log_prefix run from shell" { + run bash -c ". $LIB && set_log_prefix" + print_on_err + + [[ "$output" == "bash" ]] +} + +@test "set_log_prefix subshells ignored for caller stack" { + # nested subshells and command expansion example + # - we still expect the log prefix to indicate foo() and not the subshell + script='. $LIB ; foo() { ( ( o=$(set_log_prefix) ; echo "$o") ) ; } ; foo' + local f=""; f=$(mkscript "$script") + run $f + print_on_err + + [[ "$output" == "$(basename $f):foo()" ]] +} + +@test "set_log_prefix run in sourced file" { + # ... should yield $f2:source() - source is a special bash call stack id + local f2=""; f2=$(mkscript "set_log_prefix") + local f1=""; f1=$(mkscript ". $LIB ; main() { . $f2 ; } ; main") + run $f1 + print_on_err + + [[ "$output" == "$(basename $f2):source()" ]] +} + +@test "set_log_prefix DEBUG_ABS_PATHS set" { + local f="" ; f=$(mkscript ". $LIB ; DEBUG_ABS_PATHS=true set_log_prefix") + run $f + print_on_err + + [[ "$output" == "$(realpath -- ${f}):main()" ]] +} + +@test "set_log_prefix FROM_STACKFRAME of func declaring set_log_prefix" { + # ... setup + FROM_STACKFRAME=0 + local f3=""; f3=$(mkscript "f3_main() { set_log_prefix ; } ;") + local f2=""; f2=$(mkscript "f2_main() { . $f3 ; f3_main ; } ; ") + local f1=""; f1=$(mkscript ". $LIB ; f1_main() { . $f2 ; f2_main ; } ; f1_main") + + # ... run + run $f1 + print_on_err + + # ... verify + [[ "$output" == "$(basename $f3):f3_main()" ]] +} + +@test "set_log_prefix FROM_STACKFRAME for parent of func declaring set_log_prefix" { + # ... setup + FROM_STACKFRAME=1 + local f3=""; f3=$(mkscript "f3_main() { set_log_prefix ; } ;") + local f2=""; f2=$(mkscript "f2_main() { . $f3 ; f3_main ; } ; ") + local f1=""; f1=$(mkscript ". $LIB ; f1_main() { . $f2 ; f2_main ; } ; f1_main") + + # ... run + run $f1 + print_on_err + + # ... verify + [[ "$output" == "$(basename $f2):f2_main()" ]] +} + +@test "set_log_prefix FROM_STACKFRAME for grandparent of func declaring set_log_prefix" { + # ... setup + FROM_STACKFRAME=2 + local f3=""; f3=$(mkscript "f3_main() { set_log_prefix ; } ;") + local f2=""; f2=$(mkscript "f2_main() { . $f3 ; f3_main ; } ; ") + local f1=""; f1=$(mkscript ". $LIB ; f1_main() { . $f2 ; f2_main ; } ; f1_main") + + # ... run + run $f1 + print_on_err + + # ... verify + [[ "$output" == "$(basename $f1):f1_main()" ]] +} + diff --git a/bash/bats/habitual/std/source_files.bats b/bash/bats/habitual/std/source_files.bats new file mode 100644 index 0000000..a73d823 --- /dev/null +++ b/bash/bats/habitual/std/source_files.bats @@ -0,0 +1,133 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 + export TMPDIR=$BATS_TMPDIR/$BATS_TEST_NAME + mkdir -p $TMPDIR || true +} + +teardown() { + rm -rf $BATS_TMPDIR/$BATS_TEST_NAME || true +} + +@test "source_files can source multiple files" { + local f1="" f2="" # temp files to source + + # ... setup + f1=$(mktemp) ; f2=$(mktemp) + echo "echo 'foo'" > $f1 + echo "echo 'bar'" > $f2 + + # ... run + run source_files $f1 $f2 + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q '^foo bar$' +} + +@test "source_files can handle spaces in filenames" { + local f1="" f2="" # temp files to source + + # ... setup + TMPDIR="$TMPDIR/space in name" + mkdir -p "$TMPDIR" ; f1="$(mktemp)" ; f2="$(mktemp)" + + echo "echo 'foo'">"$f1" ; echo "echo 'bar'">"$f2" + + # ... run + run source_files "$f1" "$f2" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q '^foo bar$' +} + +@test "source_files fail if file with bad syntax" { + local f1="" f2="" f3="" # temp files to source + + # ... setup + f1="$(mktemp)" ; f2="$(mktemp)" ; f3="$(mktemp)" + echo "echo 'foo'">$f1 + echo 'flumpty'>$f2 # flumpty is a bad command. Trust me. + echo "echo 'bar'">$f3 + + # ... run + run source_files $f1 $f2 $f3 + print_on_err + + # ... verify + [[ $status -ne 0 ]] + + echo "$output" | grep -qP "flumpty: command not found" + echo "$output" | grep -qP "can not source $f2" + +} + +@test "source_files fail if file does not exist" { + local f1="" f2="" f3="" # temp files to source + + # ... setup + f1="$(mktemp)" ; f2="$(mktemp)" ; f3="$(mktemp)" + echo "echo 'foo'">$f1 + echo 'flumpty'>$f2 # flumpty is a bad command. Trust me. + rm $f3 + + # ... run + run source_files $f1 $f2 $f3 + print_on_err + + # ... verify + [[ $status -ne 0 ]] + + echo "$output" | grep -qP " $f3 does not exist" +} + +@test "source_files IGNORE_MISSING will skip missing files" { + local f1="" f2="" f3="" # temp files to source + + # ... setup + f1="$(mktemp)" ; f2="$(mktemp)" ; f3="$(mktemp)" + rm $f2 + echo "echo 'foo'">$f1 + echo "echo 'bar'">$f3 + + # ... run + local IGNORE_MISSING=true + run source_files $f1 $f2 $f3 + print_on_err + + # ... verify + [[ $status -eq 0 ]] + echo $output | grep -q '^foo bar$' +} + +@test "source_files IGNORE_MISSING still fail on bad syntax in source" { + local f1="" f2="" f3="" # temp files to source + + # ... setup + f1="$(mktemp)" ; f2="$(mktemp)" ; f3="$(mktemp)" + echo "echo 'foo'">$f1 + rm $f2 + echo 'flumpty'>$f3 # flumpty is a bad command. Trust me. + + # ... run + local IGNORE_MISSING=true + run source_files $f1 $f2 $f3 + print_on_err + + # ... verify + [[ $status -ne 0 ]] + + echo "$output" | grep -qP "flumpty: command not found" + echo "$output" | grep -qP "can not source $f3" +} + diff --git a/bash/bats/habitual/std/str_to_safe_chars.bats b/bash/bats/habitual/std/str_to_safe_chars.bats new file mode 100644 index 0000000..0fe1c7e --- /dev/null +++ b/bash/bats/habitual/std/str_to_safe_chars.bats @@ -0,0 +1,219 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo -e "START OUTPUT--|$output|--END OUTPUT" + if [[ ! -z "$EXPECTED_STR" ]]; then + [[ -z "$result_str_len" ]] || echo -e " (len: $result_str_len)\n" + echo "EXPECTED --|$EXPECTED_STR|--END OUTPUT (len: $EXPECTED_STR_LEN)" + else + echo -e "\n" + fi + echo "status: $status" +} + +setup() { + + export STR_ASCII="$(ascii_chars)" + export SAFE_AWS_CHARS='[:alnum:]:_.=+@/-' + + e="___________+_-./0123456789" + e="${e}:__=__@ABCDEFGHIJKLMNOPQRSTUVWXYZ" + e="${e}______abcdefghijklmnopqrstuvwxyz______@" + + export EXPECTED_STR="$e" + export EXPECTED_STR_LEN="${#EXPECTED_STR}" + + + export UTF8_ACUTE_E="$(printf '\xC3\xA9')" # e with acute accent + + export STR_UTF8_FLATTENED="cafe attache mate trema" + _e="$UTF8_ACUTE_E" ; STR_UTF8="caf$_e attach$_e mat$_e tr${_e}ma" + export STR_UTF8 + + . habitual/std.functions || return 1 +} + +set_posix() { + loc="POSIX" + export LC_ALL="$loc" LC_CTYPE="$loc" LANG="$loc" LANGUAGE="$loc" +} + +set_utf8() { + loc="en_US.UTF-8" + export LC_ALL="$loc" LC_CTYPE="$loc" LANG="$loc" LANGUAGE="$loc" +} + +ascii_chars() { + for((i=32;i<=127;i++)) do + printf \\$(printf '%03o\t' "$i") + done + printf " @\n" # add space to set of ascii chars, with additional at symbol + # just so we know where to look in related test failures. +} + +@test "str_to_safe_chars default replacement str is underscore" { + # ... setup + set_posix + + # ... run + run str_to_safe_chars "$STR_ASCII" + result_str_len="${#output}" + + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} + +no_success_output_expected() { + unset EXPECTED_STR + unset EXPECTED_STR_LEN +} + +@test "str_to_safe_chars fails if replacement str is too long" { + # ... setup + set_posix + no_success_output_expected + + # ... run + run str_to_safe_chars "some string" "__" + print_on_err + + # ... verify + [[ $status -ne 0 ]] + echo $output | grep -q 'replacement must be one UTF-8 char' +} + +@test "str_to_safe_chars can create safe aws tag" { + # ... setup + set_posix + + # ... run + run str_to_safe_chars "$STR_ASCII" "_" "$SAFE_AWS_CHARS" + result_str_len="${#output}" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} + +@test "str_to_safe_chars disallowed chars set not including exclamation" { + # ... setup + set_posix + EXPECTED_STR='apple____' ; EXPECTED_STR_LEN="${#EXPECTED_STR}" + STR_ASCII='apple!$*&' + ONLY_REPLACE_THESE_CHARS='![:punct:][:blank:]' + + # ... run + run str_to_safe_chars "$STR_ASCII" "_" "$SAFE_AWS_CHARS" + result_str_len="${#output}" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} + +@test "str_to_safe_chars disallowed chars set including ! as 1st of set" { + # ... setup + set_posix + EXPECTED_STR='apple_$_&_' ; EXPECTED_STR_LEN="${#EXPECTED_STR}" + STR_ASCII='apple!$*& ' + ONLY_REPLACE_THESE_CHARS='!!*[:blank:]' + + # ... run + run str_to_safe_chars "$STR_ASCII" "_" "$ONLY_REPLACE_THESE_CHARS" + result_str_len="${#output}" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} + +@test "str_to_safe_chars disallowed chars set including ! not first in set" { + # ... setup + set_posix + STR_ASCII='apple!$*& ' + EXPECTED_STR='apple_$_&_' ; EXPECTED_STR_LEN="${#EXPECTED_STR}" + ONLY_REPLACE_THESE_CHARS='!*![:blank:]' + + # ... run + run str_to_safe_chars "$STR_ASCII" "_" "$ONLY_REPLACE_THESE_CHARS" + result_str_len="${#output}" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} + +@test "str_to_safe_chars replace ascii e with utf8 acute e" { + # ... setup + set_utf8 + EXPECTED_STR="$STR_UTF8" ; EXPECTED_STR_LEN="${#EXPECTED_STR}" + + # ... run + run str_to_safe_chars "$STR_UTF8_FLATTENED" "$UTF8_ACUTE_E" '!e' + result_str_len="${#output}" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} + +@test "str_to_safe_chars replace utf8 acute e with normal e" { + # ... setup + set_utf8 + EXPECTED_STR="$STR_UTF8_FLATTENED" ; EXPECTED_STR_LEN="${#EXPECTED_STR}" + + # ... run + run str_to_safe_chars "$STR_UTF8" "e" "!$UTF8_ACUTE_E" + result_str_len="${#output}" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} + +@test "str_to_safe_chars fails with utf8 replacement with wrong locale" { + # ... setup + no_success_output_expected + set_posix + + # ... run + run str_to_safe_chars "$STR_UTF8_FLATTENED" "$UTF8_ACUTE_E" '!e' + print_on_err + + # ... verify + [[ $status -ne 0 ]] + echo $output | grep -iq 'set the correct locale' +} + +@test "str_to_safe_chars transforms utf8 char wrongly if using the wrong locale" { + # ... setup + set_posix + EXPECTED_STR='cafee attachee matee treema' + EXPECTED_STR_LEN="${#EXPECTED_STR}" + # ... run + run str_to_safe_chars "$STR_UTF8" 'e' "!${UTF8_ACUTE_E}" + result_str_len="${#output}" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "$EXPECTED_STR" ]] + [[ $result_str_len -eq $EXPECTED_STR_LEN ]] +} diff --git a/bash/bats/habitual/std/trim_str.bats b/bash/bats/habitual/std/trim_str.bats new file mode 100644 index 0000000..3fee340 --- /dev/null +++ b/bash/bats/habitual/std/trim_str.bats @@ -0,0 +1,76 @@ +#!/usr/bin/env bats +# vim: et sr sw=4 ts=4 smartindent syntax=sh: +# +print_on_err() { + echo "START OUTPUT--|$output|--END OUTPUT" + echo "status: $status" +} + +setup() { + . habitual/std.functions || return 1 +} + +@test "trim_str removes leading whitespace" { + # ... setup + str=" leading spaces" + + # ... run + run std::trim_str "$str" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "leading spaces" ]] +} + +@test "trim_str removes trailing whitespace" { + # ... setup + str="trailing spaces " + + # ... run + run std::trim_str "$str" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "trailing spaces" ]] +} + +@test "trim_str only changes trailing and leading whitespace" { + # ... setup + str="no trailing or leading spaces" + + # ... run + run std::trim_str "$str" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "no trailing or leading spaces" ]] +} + +@test "trim_str removes leading or trailing tabs" { + # ... setup + str=$(echo -e "\t remove leading and trailing tabs too\t ") + + # ... run + run std::trim_str "$str" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "remove leading and trailing tabs too" ]] +} + +@test "trim_str removes leading or trailing newlines" { + # ... setup + str=$(echo -e "\n \n remove leading and trailing newlines too \n\n ") + + # ... run + run std::trim_str "$str" + print_on_err + + # ... verify + [[ $status -eq 0 ]] + [[ "$output" == "remove leading and trailing newlines too" ]] +} diff --git a/bash/habitual/git.functions b/bash/habitual/git.functions index 83e1665..7563fa3 100644 --- a/bash/habitual/git.functions +++ b/bash/habitual/git.functions @@ -12,6 +12,10 @@ # path to git binary GIT="${GIT:-git --no-pager}" + +# valid vals: semver | taggerdate. Empty string forces lexical sort. +GIT_SORT="${GIT_SORT:-}" + # git sha1s will be truncated to this length GIT_SHA_LEN=${GIT_SHA_LEN:-8} @@ -48,8 +52,36 @@ git_sha() { # @desc Prints out the git-tag on the current commit (exact match only) # Prints empty str if there is none. +# +# Defaults to latest tag created (based on ref taggerdate) on HEAD commit. +# +# Set GIT_SORT=semver to get the highest semver tag on HEAD. +# *Non semver tags are ignored.* +# +# Set GIT_SORT=taggerdate to get the most recently applied tag on HEAD. +# +# @example +# # tags on HEAD applied in this order: zebra, aardvark, goose. +# git_tag # outputs 'goose' +# +# # tags on HEAD applied in this order: 1.3.0, 10.3.2, 0 +# git_tag # outputs 'goose' git_tag() { - ${GIT} describe --exact-match --tags 2>/dev/null || echo "" + local tags="" + if [[ "$GIT_SORT" == 'semver' ]]; then + d "... using semver sorting for tags on HEAD commit. Ignoring non-semver tags." + ${GIT} -c versionsort.suffix=- -c versionsort.suffix=+ tag \ + --points-at HEAD \ + --sort=-version:refname \ + 2>/dev/null | grep -P '^\d+\.\d+\.\d+' | head -n 1 || echo "" + elif [[ "$GIT_SORT" == 'taggerdate' ]]; then + d "... using taggerdate to find latest tag on HEAD commit" + d "... this means annotated commits take precedence over lightweight ones" + ${GIT} tag --points-at HEAD --sort=-taggerdate 2>/dev/null | head -n 1 + else + d "... using default lexical sort, or value of tag.sort git config" + ${GIT} tag --points-at HEAD --sort=-refname 2>/dev/null | head -n 1 + fi } # @desc Prints user.name (from git config) @@ -157,7 +189,7 @@ git_vars() { # @section VALIDATION FUNCTIONS ############################################################### -# @desc runs [check\_for\_changes](#check_for_changes) and +# @desc runs [check\_for\_changes](#check_for_changes) and # [sha_in_origin](#sha_in_origin) for **current dir**. # # If $DEVMODE is set, the checks will be skipped. @@ -198,8 +230,8 @@ check_for_changes() { __cd $d || exit 1 ! in_git_clone && red_e "$(pwd) is not a git dir" && return 1; - git &>/dev/null --no-pager status # make sure index is up-to-date - if git diff-index --quiet HEAD -- + $GIT status &>/dev/null # force git index to be up-to-date + if $GIT diff-index --quiet HEAD -- then i "... none found." else diff --git a/bash/habitual/git.functions.md b/bash/habitual/git.functions.md index ee62c21..7bc04be 100644 --- a/bash/habitual/git.functions.md +++ b/bash/habitual/git.functions.md @@ -22,6 +22,10 @@ * reads env var `$GIT` * or default val: `git --no-pager` +* `$GIT_SORT`: _valid vals: semver | taggerdate. Empty string forces lexical sort._ + * reads env var `$GIT_SORT` + * or default val: `empty string` + * `$GIT_SHA_LEN`: _git sha1s will be truncated to this length_ * reads env var `$GIT_SHA_LEN` * or default val: `8` @@ -77,6 +81,24 @@ Prints sha of current commit - up to $GIT\_SHA\_LEN chars. Prints out the git-tag on the current commit (exact match only) Prints empty str if there is none. +Defaults to latest tag created (based on ref taggerdate) on HEAD commit. + +Set GIT_SORT=semver to get the highest semver tag on HEAD. +*Non semver tags are ignored.* + +Set GIT_SORT=taggerdate to get the most recently applied tag on HEAD. + +#### Example + +```bash +# tags on HEAD applied in this order: zebra, aardvark, goose. +git_tag # outputs 'goose' + +# tags on HEAD applied in this order: 1.3.0, 10.3.2, 0 +git_tag # outputs 'goose' +``` + + --- ### git\_user() @@ -156,7 +178,7 @@ echo "I am in a local clone of $GIT_REPO on branch $GIT_BRANCH" --- ### no\_unpushed\_changes() -runs [check\_for\_changes](#check_for_changes) and +runs [check\_for\_changes](#check_for_changes) and [sha_in_origin](#sha_in_origin) for **current dir**. If $DEVMODE is set, the checks will be skipped. diff --git a/bash/habitual/std.functions b/bash/habitual/std.functions index 6fd2ed3..5cb4617 100644 --- a/bash/habitual/std.functions +++ b/bash/habitual/std.functions @@ -169,7 +169,13 @@ std::trim_str() { std::render_tmpl() { local file_tmpl="${1:-$file_tmpl}" local allow_code="${2:-$allow_code}" # default is to not execute code on render. - required_vars "file_tmpl" || return 1 + + if ! check_var_defined "file_tmpl" + then + e "... expects either /path/to/file as arg or as env var \$file_tmpl" + return 1 + fi + [[ ! -r "$file_tmpl" ]] && e "$file_tmpl is not readable" && return 1 local d="" delim="__render_tmpl__" cmd="" @@ -264,7 +270,14 @@ str_to_safe_chars() { local p="${3:-$(safe_chars_def_list)}" # allowed chars (or with leading ! disallowed) [[ -z "$1" ]] && red_e "... you must pass a str to transform" && return 1 - [[ "${#r}" -ne 1 ]] && red_e "... replacement must be one UTF-8 char, not '$r'" && return 1 + if [[ "${#r}" -ne 1 ]]; then + msg="... replacement must be one UTF-8 char, not '$r'" + msg="$msg\nDid you set the correct locale? e.g. if str or replacement is utf8" + msg="$msg\nensure your locale is also utf8." + red_e "$msg" + + return 1 + fi local sed_class="[^$p]" # if user passed a list of disallowed chars @@ -308,16 +321,27 @@ safe_chars_def_list() { # envsubst_tokens_list() { local tmpl_tokens="$1" - local list="" + local list="" invalid_tokens rc=0 for token in $tmpl_tokens; do - token="\${$token}" - if [[ -z "$list" ]]; then - list="$token" + if [[ "$token" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then + token="\${$token}" + if [[ -z "$list" ]]; then + list="$token" + else + list="$list $token" + fi else - list="$list $token" + invalid_tokens="$invalid_tokens $token" + rc=1 fi done - echo "$list" + + if [[ $rc -eq 0 ]]; then + echo "$list" + else + e "following tokens are invalid var names:$invalid_tokens" + fi + return $rc } # @desc creates random str of format -- @@ -346,39 +370,79 @@ random_str() { semver_a_ge_b() { local a="$1" local b="$2" - local va="${1##[vV]}" ; va="${va%%-*}" # strip leading v and any prerelease info etc - local vb="${2##[vV]}" ; vb="${vb%%-*}" # strip leading v and any prerelease info etc - local p='^[vV]?[0-9]+\.[0-9]+\.[0-9]+(-.+)?$' + local p='^[vV]?[0-9]+\.[0-9]+\.[0-9]+([-\+].+)?$' ! [[ "$a" =~ $p ]] && e "... expects 2 semver strs as params" && return 2 ! [[ "$b" =~ $p ]] && e "... expects 2 semver strs as params" && return 2 + local va="${1##[vV]}" ; va="${va%%[-+]*}" # strip v and prerelease / build metadata + local vb="${2##[vV]}" ; vb="${vb%%[-+]*}" # strip v and prerelease / build metadata + # prerelease versions have a lower precedence than normal # so if x.y.z are same for each, but one is a prerelease, it is less. # if x.y.z are same but both are prereleases, normal comparison. if [[ "$va" == "$vb" ]] && [[ "$a$b" =~ \- ]]; then - if [[ "$a" =~ \- ]] && [[ "$b" =~ \- ]]; then - - # ... both are prerelease, just compare originals with out leading vVs - _semver_a_gt_b ${a##[vV]} ${b##[vV]} + if [[ "$a" =~ \- ]] && [[ "$b" =~ \- ]]; then # 'a' and 'b' are prereleases + _compare_semver_metadata "${a#*-}" "${b#*-}" # compare prerelease/build data return $? - elif [[ "$a" =~ \- ]] ; then - return 1 + return 1 # 'a' is the prerelease else - return 0 + return 0 # 'b' is the prerelease fi fi - _semver_a_gt_b $va $vb + _semver_a_ge_b $va $vb } -_semver_a_gt_b() { +_semver_a_ge_b() { local a=$1 local b=$2 - [[ "$a" == "$b" ]] || [[ $(echo -e "$a\n$b" | sort -V | head -n 1) != "$a" ]] + [[ "$a" == "$b" ]] || [[ $(echo -e "$a\n$b" | sort -V | tail -n 1) == "$a" ]] +} + +# _compare_semver_metadata only used when 'a' and 'b' are both prereleases +_compare_semver_metadata() { + local a="$1" + local b="$2" + local rc=0 sort_opt="" + + # See https://semver.org/#spec-item-11 for the precedence rules. + + # Build metadata is ignored for precedence + a="${a%%+*}"; b="${b%%+*}" + + # the one with more dot separators wins. + dots_a="${a//[^\.]}" ; dots_b="${b//[^\.]}" + + # same number of dots. Now come the nasty semver rules + # ... compare each dot-separated part from left to right. + # If one has numbers only - no leading zeros, but the other alphas, alphas win. + # If both are alphas, normal ascii sort + # If both are numbers, numeric sort + a="${a}." ; b="${b}." + while [[ "$a" ]]; do + ax="${a%%.*}" ; bx="${b%%.*}" + if [[ "$ax" == "$bx" ]]; then + a="${a#*.}" ; b="${b#*.}" # remove compared bits from str + continue # move on to next part of metadata + fi + + if [[ "${ax}X${bx}" =~ ^[1-9][0-9]*X[1-9][0-9]*$ ]]; then + sort_opt="-n" + fi + if [[ $(echo -e "$ax\n$bx" | sort $sort_opt | head -n1) == "$ax" ]]; then + rc=1 + fi + break + done + + # if b still has subparts but a does not, then b wins. + [[ "${a}" =~ ^\.*$ ]] && [[ ${#b} -gt ${#a} ]] && rc=1 + + return $rc } # @desc Exports $BUILD_URL if available from a number of possible sources. @@ -387,11 +451,12 @@ _semver_a_gt_b() { # # Returns 1 if BUILD_URL can not be determined. # -# Use this to annotate your builds and deployments with governance metadata. e.g. the job run -# should show you who built what when. +# Use this to annotate your builds and deployments with governance metadata. +# e.g. the job run should show you who built what when. # -# [shippable](https://shippable.com), [circleci](https://circleci.com) and [jenkins](https://jenkins.io) -# provide an equivalent var. This func just exports it with a standard name. +# [shippable](https://shippable.com), [circleci](https://circleci.com) and +# [jenkins](https://jenkins.io) provide an equivalent var. +# This func just exports it with a standard name. # # TravisCI [does not](https://github.com/travis-ci/travis-ci/issues/8935), but it is # possible to construct it. @@ -399,13 +464,18 @@ _semver_a_gt_b() { export_build_url() { if [[ ! -z "$CIRCLE_BUILD_URL" ]]; then BUILD_URL="$CIRCLE_BUILD_URL" - elif [[ "$TRAVIS" == "true" ]]; then - if required_vars "TRAVIS_REPO_SLUG TRAVIS_JOB_ID" - then - BUILD_URL="https://travis-ci.org/$TRAVIS_REPO_SLUG/jobs/$TRAVIS_JOB_ID" - fi + elif [[ ! -z "$TRAVIS_JOB_WEB_URL" ]]; then + BUILD_URL="$TRAVIS_JOB_WEB_URL" + elif [[ ! -z "$CI_BUILD_URL" ]]; then + BUILD_URL="$CI_BUILD_URL" # for codeship fi - [[ ! -z "$BUILD_URL" ]] && export BUILD_URL + + [[ -z "$BUILD_URL" ]] && return 1 + + d "BUILD_URL:[$BUILD_URL]" + + export BUILD_URL + return 0 } ##################################################################### @@ -479,7 +549,6 @@ set_log_prefix() { fi } - # @desc prints ERROR to STDERR, with context prefix and # stacktrace. # diff --git a/bash/habitual/std.functions.md b/bash/habitual/std.functions.md index 87fdf6f..dc11628 100644 --- a/bash/habitual/std.functions.md +++ b/bash/habitual/std.functions.md @@ -347,11 +347,12 @@ $BUILD_URL is a link to a CI/CD job's run. Returns 1 if BUILD_URL can not be determined. -Use this to annotate your builds and deployments with governance metadata. e.g. the job run -should show you who built what when. +Use this to annotate your builds and deployments with governance metadata. +e.g. the job run should show you who built what when. -[shippable](https://shippable.com), [circleci](https://circleci.com) and [jenkins](https://jenkins.io) -provide an equivalent var. This func just exports it with a standard name. +[shippable](https://shippable.com), [circleci](https://circleci.com) and +[jenkins](https://jenkins.io) provide an equivalent var. +This func just exports it with a standard name. TravisCI [does not](https://github.com/travis-ci/travis-ci/issues/8935), but it is possible to construct it. diff --git a/shippable.build.sh b/shippable.build.sh old mode 100644 new mode 100755 index 211914e..7a5e99a --- a/shippable.build.sh +++ b/shippable.build.sh @@ -1,45 +1,113 @@ #!/bin/bash # vim: et sr sw=4 ts=4 smartindent: -CIUSER_HOME=/home/ciuser -CIUSER_BUILD_DIR=$CIUSER_HOME/build -NVM_DIR=$CIUSER_HOME/.nvm +BATS_GIT_URL="${BATS_GIT_URL:-https://github.com/bats-core/bats-core}" +BATS_GIT_REF="${BATS_GIT_REF:-master}" install_tools() { - sudo apt-get update - sudo apt-get install -y coreutils realpath - sort --help | grep -q -- '--version-sort' || return 1 # ...verify coreutils - realpath $PWD >/dev/null || return 1 # ... verify realpath + export DEBIAN_FRONTEND=noninteractive + apt-get -qq update + apt_get_if_missing "coreutils realpath" || return 1 + + _install_test_tools || return 1 } -# workarounds because of quirky behaviour when building -# on shippable nodes. -shippable_hacks() { - hack_nvm_sh || return 1 +coreutils_exists() { + # ... check if sort can handle semver - that's a GNU extension + ( set -o pipefail ; sort --help | grep -q -- '--version-sort' ) } -# hack_nvm_sh() : -# Required to suppress spurious error on standard shippable build node -# when running with non-root user. -# The standard shippable build node will call nvm.sh -# any time a user shell is invoked, but only root has nvm.sh available. -hack_nvm_sh() { - mkdir $NVM_DIR 2>/dev/null - echo '#!/bin/bash' > $NVM_DIR/nvm.sh - chmod a+x $NVM_DIR/nvm.sh - chown -R ciuser:ciuser $NVM_DIR +realpath_exists() { realpath $(pwd) ; } + +parallel_exists() { parallel --version ; } + +libxml2_utils_exists() { xmllint --version ; } + +apt_get_if_missing() { + local pkgs="$1" + local rc=0 + local pkg="" to_install="" verify_func="" + + for pkg in $pkgs; do + verify_func="${pkg//-/_}_exists" + $verify_func &>/dev/null || to_install="$pkg $to_install" + done + + if [[ ! -z "$to_install" ]]; then + echo "INFO: will install $to_install" + apt-get -y install $to_install + fi + + for pkg in $to_install; do + verify_func="${pkg//-/_}_exists" + if ! $verify_func &>/dev/null + then + echo >&2 "ERROR: could not install $pkg" + rc=1 + fi + done + + return $rc +} + +_install_test_tools() { + _install_bats \ + && _install_strip_ansi_cli \ + && _install_tap_xunit +} + +# ... _install_bats, also installs parallel +_install_bats() { + local d=/var/tmp/bats-core + git clone --depth 1 --branch $BATS_GIT_REF $BATS_GIT_URL $d + ( + cd $d + local rc=0 + + ./install.sh /usr/local + if ! bats --version &>/dev/null + then + echo >&2 "ERROR: could not install bats" + rc=1 + fi + rm -rf $d &>/dev/null + + apt_get_if_missing "parallel" || rc=1 + exit $rc + ) +} + +_install_strip_ansi_cli() { + npm i -g --silent strip-ansi-cli + vstr="\033[1mFoo Bar\033[0m" + + if [[ "$(echo -e "$vstr" | strip-ansi 2>/dev/null)" != "Foo Bar" ]]; then + echo >&2 "ERROR: could not install strip-ansi-cli" + return 1 + else + return 0 + fi } -prepare_ciuser() { - adduser --disabled-password --gecos "" ciuser || return 1 - cp -r $SHIPPABLE_BUILD_DIR $CIUSER_BUILD_DIR || return 1 - chown -R ciuser:ciuser $CIUSER_BUILD_DIR || return 1 +_install_tap_xunit() { + npm i -g --silent tap-xunit + # ... libxml2 provides xmllint for verification + apt_get_if_missing "libxml2-utils" || return 1 + vstr='1..2\nok 1 foo test\nok 2 bar test\n' + ( + set -o pipefail + if ! echo -e "$vstr" | tap-xunit | xmllint --format - &>/dev/null + then + echo "ERROR: could not verify tap-junit install" + return 1 + else + return 0 + fi + ) } prepare_env() { install_tools || return 1 - prepare_ciuser || return 1 - shippable_hacks || return 1 } build() { return 0 ; } # nothing to build yet diff --git a/shippable.create_ciuser.sh b/shippable.create_ciuser.sh new file mode 100755 index 0000000..7fb2823 --- /dev/null +++ b/shippable.create_ciuser.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# vim: et sr sw=4 ts=4 smartindent: +# +# This script creates a user to run the tests +# as some require non-superuser permissions +# to work e.g. testing a file is unreadable +# +prepare_ciuser() { + adduser --disabled-password --gecos "" ciuser || return 1 + cp -r $SHIPPABLE_BUILD_DIR $CIUSER_BUILD_DIR || return 1 + chown -R ciuser:ciuser $CIUSER_BUILD_DIR || return 1 +} + +# workarounds because of quirky behaviour when building +# on shippable nodes. +shippable_hacks() { + hack_nvm_sh || return 1 +} + +# hack_nvm_sh() : +# Required to suppress spurious error on standard shippable build node +# when running with non-root user. +# The standard shippable build node will call nvm.sh +# any time a user shell is invoked, but only root has nvm.sh available. +hack_nvm_sh() { + mkdir $NVM_DIR 2>/dev/null + echo '#!/bin/bash' > $NVM_DIR/nvm.sh + chmod a+x $NVM_DIR/nvm.sh + chown -R ciuser:ciuser $NVM_DIR +} + +main() { + # ... required vars + [[ -z "$CIUSER_BUILD_DIR" ]] && exit 1 + [[ -z "$NVM_DIR" ]] && exit 1 + + prepare_ciuser && shippable_hacks +} + +main diff --git a/shippable.test.sh b/shippable.test.sh old mode 100644 new mode 100755 index 7d9b27e..45a3318 --- a/shippable.test.sh +++ b/shippable.test.sh @@ -1,35 +1,60 @@ #!/bin/bash # vim: et sr sw=4 ts=4 smartindent: -CIUSER_HOME=/home/ciuser -CIUSER_BUILD_DIR=$CIUSER_HOME/build -BASH_LIBS_DIR=$CIUSER_BUILD_DIR/bash +CIUSER_RESULTS_DIR="${CIUSER_RESULTS_DIR:-/home/ciuser/shippable/testresults}" +CONCURRENT_TESTS="${CONCURRENT_TESTS:-4}" +BASH_LIBS_DIR="${BASH_LIBS_DIR:-/home/ciuser/build/bash}" +TEST_ROOT="${TEST_ROOT:-bats}" # path to file or dir containing bats tests -find_libs_to_test() { - find . -path './t' -prune -o -name '*.functions' -print -} +run_bash_tests() { + local rc=0 -run_bash_test_file() { - local lib="$1" - local f="t/${lib#./}" + # ... run bats tests for console log + ( + set -o pipefail + bats -r -t -j $CONCURRENT_TESTS bats \ + | tee bats.tap + ) + rc=$? - [[ ! -x $f ]] && echo "INFO: no tests for $lib in $f" && return 0 + # ... bats output => junit-compatible xml + # We are using node tap-xunit module because it parses + # test run STDOUT/STDERR better than the tap-junit module. + cat bats.tap \ + | awk -f bats/bats_tap12_to_tap13.awk \ + | strip-ansi \ + | tap-xunit > $CIUSER_RESULTS_DIR/bash_test_results.xml - echo -e "\n\nINFO: RUNNING $f as $CIUSER in $BASH_LIBS_DIR ..." - su -c "cd $BASH_LIBS_DIR && $f" ciuser && return 0 + rm bats.tap - echo >&2 "ERROR: failure from $lib" - return 1 + return $rc } -run_bash_tests() { - local rc=0 - for lib in $(find_libs_to_test); do - run_bash_test_file "$lib" || rc=1 - done - return $rc +# ... required by the git.functions tests +# +# Must be run before kicking off bats with multiple concurrent +# jobs or else race-conditions between tests and the repo +# creation. +# +create_tmpl_repo() { + local tmpl_repo="/var/tmp/opsgang/libs/repo" + local src_repo_url="https://github.com/opsgang/libs" + + [[ -d $tmpl_repo ]] && rm -rf $tmpl_repo + mkdir -p $(dirname $tmpl_repo) + git clone --depth 5 --branch master $src_repo_url $tmpl_repo &>/dev/null + + (cd $tmpl_repo && git reset --hard >/dev/null) + + return 0 } main() { + mkdir -p $CIUSER_RESULTS_DIR || return 1 + echo "INFO: will store test results in $CIUSER_RESULTS_DIR" + + echo "INFO: creating template repo for habitual/git.functions tests" + create_tmpl_repo ; ls -ld /var/tmp/opsgang/libs/repo + echo "INFO: running tests for bash libs" ( cd $BASH_LIBS_DIR && run_bash_tests ) } diff --git a/shippable.yml b/shippable.yml index 579cff2..62e913c 100644 --- a/shippable.yml +++ b/shippable.yml @@ -5,12 +5,26 @@ language: none env: - - secure: gX7Rt/i0/C05NCD2XS1gTssLtul+rbRdEdtuNSFpegu8Fz85PpAnB+MQlirqQ9M9ObPSSVn599EJo0QUX1Hxx1oBvW11jA7p8iyLea6agEVLY7KBT1rA95TX81O3UwYECpN/hoiDih6hAwRL/em6yGvjqREYkZKKhn0foJ9RM7Xb74BCW8lxEZSSk4JFJssv2PNGgDhbBKfcD1Xk5sqTINleC3x/IpntyphCBFNso3GUOquRSVGtnhU+fWtHSZ2YAr6W3Jy9fUaTcUDbQ/WFJkevmQX7ubCPzuusfZdygUsOQtrbi0vP31kR2vomqH6GKo9pLdCavjlCig1xR2VqqA== + global: + - CIUSER_HOME: /home/ciuser + - CIUSER_BUILD_DIR: /home/ciuser/build + - CIUSER_RESULTS_DIR: /home/ciuser/shippable/testresults + - BASH_LIBS_DIR: /home/ciuser/build/bash + - NVM_DIR: /home/ciuser/.nvm + - CONCURRENT_TESTS: 4 + - TEST_ROOT: bats/ build: ci: - - chmod a+x shippable.*.sh ; ( ./shippable.build.sh && ./shippable.test.sh ) + - chmod a+x shippable.*.sh ; + ( + ./shippable.build.sh + && ./shippable.create_ciuser.sh + && su -c "./shippable.test.sh" ciuser ; rc=$? ; + cp -r ${CIUSER_RESULTS_DIR}/*.xml shippable/testresults ; + exit $rc + ) on_success: ./bash/bundles/release