diff --git a/lib/compose.sh b/lib/compose.sh index 187c4bf..4b47bd4 100644 --- a/lib/compose.sh +++ b/lib/compose.sh @@ -39,22 +39,31 @@ myos_compose() { done _dir=$(dirname "$_first") - # variables the compose files reference, plus the network names they default - # on (networks.yml is appended after the scan, so its variables are added here) + # the variables the compose files reference, plus the network names: those + # live in networks.yml, which is appended after the scan # shellcheck disable=SC2086 # both are deliberate word lists _vars=$(myos_env_vars $_files) # shellcheck disable=SC2086 _envargs=$(myos_env_export $_vars DOCKER_NETWORK_DEFAULT DOCKER_NETWORK_PRIVATE DOCKER_NETWORK_PUBLIC COMPOSE_SERVICE_NAME) if [ "${DRYRUN:-false}" = true ]; then + # shellcheck disable=SC2086 # printed, not executed printf '%s%s -p %s --project-directory %s %s\n' "$_bin" "$_fargs" "$_project" "$_dir" "$*" - else - _IFS=$IFS; IFS=' -' - # shellcheck disable=SC2046,SC2086 # deliberate word splitting on IFS=newline - env $_envargs $_bin --ansi=auto $_fargs -p "$_project" --project-directory "$_dir" "$@" - _rc=$? - IFS=$_IFS - return $_rc + return 0 fi + + # Export the variables in a subshell rather than through env(1): a value may + # contain spaces, and the command line must still be split on spaces (the + # compose binary can be the two words "docker compose"). + ( + while IFS= read -r _kv; do + [ -n "$_kv" ] || continue + # shellcheck disable=SC2163 # _kv is a NAME=value pair, not a name + export "$_kv" + done <> "$MYOS_DOCKER_LOG" +if [ -n "${MYOS_DOCKER_LOG:-}" ]; then + printf 'docker %s\n' "$*" >> "$MYOS_DOCKER_LOG" + # the environment is recorded too, so a test can assert what was exported + env > "$(dirname "$MYOS_DOCKER_LOG")/env.log" +fi case "$1 $2" in "compose version") echo "${MOCK_COMPOSE_VERSION:-2.29.0}" ;; "compose config") echo "# mock compose config" ;; diff --git a/spec/support/bin/docker-compose b/spec/support/bin/docker-compose index 1e87a70..eec4ed6 100755 --- a/spec/support/bin/docker-compose +++ b/spec/support/bin/docker-compose @@ -1,4 +1,8 @@ #!/bin/sh -[ -n "${MYOS_DOCKER_LOG:-}" ] && printf 'docker-compose %s\n' "$*" >> "$MYOS_DOCKER_LOG" +if [ -n "${MYOS_DOCKER_LOG:-}" ]; then + printf 'docker-compose %s\n' "$*" >> "$MYOS_DOCKER_LOG" + # the environment is recorded too, so a test can assert what was exported + env > "$(dirname "$MYOS_DOCKER_LOG")/env.log" +fi case "$1" in version) echo "${MOCK_COMPOSE_VERSION:-2.29.0}" ;; config) echo "# mock compose config" ;; *) : ;; esac exit 0 diff --git a/spec/unit/compose_spec.sh b/spec/unit/compose_spec.sh new file mode 100644 index 0000000..d13c3c6 --- /dev/null +++ b/spec/unit/compose_spec.sh @@ -0,0 +1,75 @@ +#shellcheck shell=sh +Include lib/str.sh +Include lib/core.sh +Include lib/tags.sh +Include lib/config.sh +Include lib/compose.sh + +# These assertions look at what is actually executed, not at what --dry-run +# prints: the two used to disagree, because the execution path set IFS to a +# newline and so never split "-f a -f b", nor the two words "docker compose". +Describe 'lib/compose.sh execution' + setup() { + MYOS_TMP=$(mktemp -d "${TMPDIR:-/tmp}/myos-compose.XXXXXX") + MYOS_TMP=$(cd "$MYOS_TMP" && pwd -P) + printf 'services:\n a:\n image: ${IMAGE}\n' > "$MYOS_TMP/a.yml" + printf 'services:\n b:\n image: alpine\n' > "$MYOS_TMP/b.yml" + # exported, otherwise the mock (a child process) never sees it + export MYOS_DOCKER_LOG=$MYOS_TMP/docker.log + export PATH=$SPEC_DIR/support/bin:$PATH + DRYRUN=false + IMAGE=alpine:3.20 + unset MYOS_COMPOSE_BIN 2>/dev/null || true + } + cleanup() { rm -rf "$MYOS_TMP"; } + BeforeEach setup + AfterEach cleanup + + logged() { cat "$MYOS_DOCKER_LOG"; } + + two_files() { myos_compose demo "$(printf '%s\n%s' "$MYOS_TMP/a.yml" "$MYOS_TMP/b.yml")" -- config; } + + It 'passes every -f as its own argument' + When call two_files + The status should be success + The result of function logged should include "-f $MYOS_TMP/a.yml -f $MYOS_TMP/b.yml" + The result of function logged should include "-p demo" + The result of function logged should include "--project-directory $MYOS_TMP" + The result of function logged should end with "config" + End + + It 'splits the two words of the docker compose plugin' + MYOS_COMPOSE_BIN="docker compose" + When call myos_compose demo "$MYOS_TMP/a.yml" -- config + The status should be success + The result of function logged should start with "docker compose --ansi=auto" + End + + It 'runs the docker-compose binary as one word' + MYOS_COMPOSE_BIN="docker-compose" + When call myos_compose demo "$MYOS_TMP/a.yml" -- config + The status should be success + The result of function logged should start with "docker-compose --ansi=auto" + End + + It 'passes the variables the compose files reference' + MYOS_COMPOSE_BIN="docker-compose" + When call myos_compose demo "$MYOS_TMP/a.yml" -- config + The status should be success + The contents of file "$MYOS_TMP/env.log" should include "IMAGE=alpine:3.20" + End + + It 'keeps a value that contains spaces in one piece' + MYOS_COMPOSE_BIN="docker-compose" + IMAGE="alpine with spaces" + When call myos_compose demo "$MYOS_TMP/a.yml" -- config + The status should be success + The contents of file "$MYOS_TMP/env.log" should include "IMAGE=alpine with spaces" + End + + It 'refuses to run without a compose file' + When call myos_compose demo "" -- config + The status should equal 3 + The stderr should include "no compose file" + End +End