diff --git a/Dockerfile b/Dockerfile index 77ec8e8da..2a4a11984 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,7 @@ ENV GO111MODULE=on RUN make get-modules ARG MAKE_TARGET=build -RUN make ${MAKE_TARGET} && \ - if [[ "${MAKE_TARGET}" == 'lint' ]]; then \ - mkdir -p /usr/src/airshipctl/bin; \ - touch /usr/src/airshipctl/bin/airshipctl; \ - fi +RUN make ${MAKE_TARGET} FROM ${RELEASE_IMAGE} as release COPY --from=builder /usr/src/airshipctl/bin/airshipctl /usr/local/bin/airshipctl diff --git a/Makefile b/Makefile index 7871ce18b..2c3c3e4b0 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,15 @@ DOCKER_IMAGE_NAME ?= airshipctl DOCKER_IMAGE_PREFIX ?= airshipit DOCKER_IMAGE_TAG ?= dev DOCKER_IMAGE ?= $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_PREFIX)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) +DOCKER_TARGET_STAGE ?= release # go options PKG := ./... TESTS := . +TEST_FLAGS := +COVER_FLAGS := COVER_PROFILE := cover.out +COVER_PKG := $(shell go list ./... | tail -n+2 | grep -v "opendev.org/airship/airshipctl/testutil" | paste -sd"," -) .PHONY: get-modules get-modules: @@ -34,17 +38,17 @@ build: get-modules .PHONY: test test: lint -test: TESTFLAGS += -race -v -test: unit-tests test: cover .PHONY: unit-tests -unit-tests: build +unit-tests: TESTFLAGS += -race -v +unit-tests: @echo "Performing unit test step..." - @GO111MODULE=on go test -run $(TESTS) $(PKG) $(TESTFLAGS) -covermode=atomic -coverprofile=$(COVER_PROFILE) + @GO111MODULE=on go test -run $(TESTS) $(PKG) $(TESTFLAGS) $(COVER_FLAGS) @echo "All unit tests passed" .PHONY: cover +cover: COVER_FLAGS = -covermode=atomic -coverprofile=$(COVER_PROFILE) -coverpkg=$(COVER_PKG) cover: unit-tests @./tools/coverage_check $(COVER_PROFILE) @@ -56,7 +60,7 @@ lint: .PHONY: docker-image docker-image: - @docker build . --build-arg MAKE_TARGET=$(DOCKER_MAKE_TARGET) --tag $(DOCKER_IMAGE) + @docker build . --build-arg MAKE_TARGET=$(DOCKER_MAKE_TARGET) --tag $(DOCKER_IMAGE) --target $(DOCKER_TARGET_STAGE) .PHONY: print-docker-image-tag print-docker-image-tag: @@ -64,10 +68,12 @@ print-docker-image-tag: .PHONY: docker-image-unit-tests docker-image-unit-tests: DOCKER_MAKE_TARGET = cover +docker-image-unit-tests: DOCKER_TARGET_STAGE = builder docker-image-unit-tests: docker-image .PHONY: docker-image-lint docker-image-lint: DOCKER_MAKE_TARGET = lint +docker-image-lint: DOCKER_TARGET_STAGE = builder docker-image-lint: docker-image .PHONY: clean @@ -81,7 +87,7 @@ docs: .PHONY: update-golden update-golden: delete-golden -update-golden: TESTFLAGS += -update -v +update-golden: TESTFLAGS += -update update-golden: PKG = opendev.org/airship/airshipctl/cmd/... update-golden: unit-tests diff --git a/cmd/completion/completion_test.go b/cmd/completion/completion_test.go new file mode 100644 index 000000000..7b56d05c5 --- /dev/null +++ b/cmd/completion/completion_test.go @@ -0,0 +1,48 @@ +package completion_test + +import ( + "errors" + "testing" + + "opendev.org/airship/airshipctl/cmd/completion" + "opendev.org/airship/airshipctl/testutil" +) + +func TestCompletion(t *testing.T) { + cmd := completion.NewCompletionCommand() + + cmdTests := []*testutil.CmdTest{ + { + Name: "completion-bash", + CmdLine: "bash", + Cmd: cmd, + }, + { + Name: "completion-zsh", + CmdLine: "zsh", + Cmd: cmd, + }, + { + Name: "completion-no-args", + CmdLine: "", + Cmd: cmd, + Error: errors.New("shell not specified"), + }, + { + Name: "completion-too-many-args", + CmdLine: "bash zsh", + Cmd: cmd, + Error: errors.New("too many arguments, expected only the shell type"), + }, + { + Name: "completion-unknown-shell", + CmdLine: "fish", + Cmd: cmd, + Error: errors.New("unsupported shell type \"fish\""), + }, + } + + for _, tt := range cmdTests { + testutil.RunTest(t, tt) + } +} diff --git a/cmd/completion/testdata/TestCompletionGoldenOutput/completion-bash.golden b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-bash.golden new file mode 100644 index 000000000..553654e82 --- /dev/null +++ b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-bash.golden @@ -0,0 +1,303 @@ +# bash completion for completion -*- shell-script -*- + +__completion_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Homebrew on Macs have version 1.3 of bash-completion which doesn't include +# _init_completion. This is a very minimal version of that function. +__completion_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +__completion_index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 +} + +__completion_contains_word() +{ + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 +} + +__completion_handle_reply() +{ + __completion_debug "${FUNCNAME[0]}" + case $cur in + -*) + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + local allflags + if [ ${#must_have_one_flag[@]} -ne 0 ]; then + allflags=("${must_have_one_flag[@]}") + else + allflags=("${flags[*]} ${two_word_flags[*]}") + fi + COMPREPLY=( $(compgen -W "${allflags[*]}" -- "$cur") ) + if [[ $(type -t compopt) = "builtin" ]]; then + [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace + fi + + # complete after --flag=abc + if [[ $cur == *=* ]]; then + if [[ $(type -t compopt) = "builtin" ]]; then + compopt +o nospace + fi + + local index flag + flag="${cur%=*}" + __completion_index_of_word "${flag}" "${flags_with_completion[@]}" + COMPREPLY=() + if [[ ${index} -ge 0 ]]; then + PREFIX="" + cur="${cur#*=}" + ${flags_completion[${index}]} + if [ -n "${ZSH_VERSION}" ]; then + # zsh completion needs --flag= prefix + eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" + fi + fi + fi + return 0; + ;; + esac + + # check if we are handling a flag with special work handling + local index + __completion_index_of_word "${prev}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + ${flags_completion[${index}]} + return + fi + + # we are parsing a flag and don't have a special handler, no completion + if [[ ${cur} != "${words[cword]}" ]]; then + return + fi + + local completions + completions=("${commands[@]}") + if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then + completions=("${must_have_one_noun[@]}") + fi + if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then + completions+=("${must_have_one_flag[@]}") + fi + COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") ) + + if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then + COMPREPLY=( $(compgen -W "${noun_aliases[*]}" -- "$cur") ) + fi + + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + declare -F __custom_func >/dev/null && __custom_func + fi + + # available in bash-completion >= 2, not always present on macOS + if declare -F __ltrim_colon_completions >/dev/null; then + __ltrim_colon_completions "$cur" + fi + + # If there is only 1 completion and it is a flag with an = it will be completed + # but we don't want a space after the = + if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then + compopt -o nospace + fi +} + +# The arguments should be in the form "ext1|ext2|extn" +__completion_handle_filename_extension_flag() +{ + local ext="$1" + _filedir "@(${ext})" +} + +__completion_handle_subdirs_in_dir_flag() +{ + local dir="$1" + pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 +} + +__completion_handle_flag() +{ + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + # if a command required a flag, and we found it, unset must_have_one_flag() + local flagname=${words[c]} + local flagvalue + # if the word contained an = + if [[ ${words[c]} == *"="* ]]; then + flagvalue=${flagname#*=} # take in as flagvalue after the = + flagname=${flagname%=*} # strip everything after the = + flagname="${flagname}=" # but put the = back + fi + __completion_debug "${FUNCNAME[0]}: looking for ${flagname}" + if __completion_contains_word "${flagname}" "${must_have_one_flag[@]}"; then + must_have_one_flag=() + fi + + # if you set a flag which only applies to this command, don't show subcommands + if __completion_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then + commands=() + fi + + # keep flag value with flagname as flaghash + # flaghash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then + if [ -n "${flagvalue}" ] ; then + flaghash[${flagname}]=${flagvalue} + elif [ -n "${words[ $((c+1)) ]}" ] ; then + flaghash[${flagname}]=${words[ $((c+1)) ]} + else + flaghash[${flagname}]="true" # pad "true" for bool flag + fi + fi + + # skip the argument to a two word flag + if __completion_contains_word "${words[c]}" "${two_word_flags[@]}"; then + c=$((c+1)) + # if we are looking for a flags value, don't show commands + if [[ $c -eq $cword ]]; then + commands=() + fi + fi + + c=$((c+1)) + +} + +__completion_handle_noun() +{ + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + if __completion_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then + must_have_one_noun=() + elif __completion_contains_word "${words[c]}" "${noun_aliases[@]}"; then + must_have_one_noun=() + fi + + nouns+=("${words[c]}") + c=$((c+1)) +} + +__completion_handle_command() +{ + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_completion_root_command" + else + next_command="_${words[c]//:/__}" + fi + fi + c=$((c+1)) + __completion_debug "${FUNCNAME[0]}: looking for ${next_command}" + declare -F "$next_command" >/dev/null && $next_command +} + +__completion_handle_word() +{ + if [[ $c -ge $cword ]]; then + __completion_handle_reply + return + fi + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + if [[ "${words[c]}" == -* ]]; then + __completion_handle_flag + elif __completion_contains_word "${words[c]}" "${commands[@]}"; then + __completion_handle_command + elif [[ $c -eq 0 ]]; then + __completion_handle_command + elif __completion_contains_word "${words[c]}" "${command_aliases[@]}"; then + # aliashash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then + words[c]=${aliashash[${words[c]}]} + __completion_handle_command + else + __completion_handle_noun + fi + else + __completion_handle_noun + fi + __completion_handle_word +} + +_completion_root_command() +{ + last_command="completion" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--help") + flags+=("-h") + local_nonpersistent_flags+=("--help") + + must_have_one_flag=() + must_have_one_noun=() + must_have_one_noun+=("bash") + must_have_one_noun+=("zsh") + noun_aliases=() +} + +__start_completion() +{ + local cur prev words cword + declare -A flaghash 2>/dev/null || : + declare -A aliashash 2>/dev/null || : + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -s || return + else + __completion_init_completion -n "=" || return + fi + + local c=0 + local flags=() + local two_word_flags=() + local local_nonpersistent_flags=() + local flags_with_completion=() + local flags_completion=() + local commands=("completion") + local must_have_one_flag=() + local must_have_one_noun=() + local last_command + local nouns=() + + __completion_handle_word +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_completion completion +else + complete -o default -o nospace -F __start_completion completion +fi + +# ex: ts=4 sw=4 et filetype=sh diff --git a/cmd/completion/testdata/TestCompletionGoldenOutput/completion-no-args.golden b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-no-args.golden new file mode 100644 index 000000000..5e588c0ca --- /dev/null +++ b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-no-args.golden @@ -0,0 +1,7 @@ +Error: shell not specified +Usage: + completion SHELL [flags] + +Flags: + -h, --help help for completion + diff --git a/cmd/completion/testdata/TestCompletionGoldenOutput/completion-too-many-args.golden b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-too-many-args.golden new file mode 100644 index 000000000..22323a2f4 --- /dev/null +++ b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-too-many-args.golden @@ -0,0 +1,7 @@ +Error: too many arguments, expected only the shell type +Usage: + completion SHELL [flags] + +Flags: + -h, --help help for completion + diff --git a/cmd/completion/testdata/TestCompletionGoldenOutput/completion-unknown-shell.golden b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-unknown-shell.golden new file mode 100644 index 000000000..f6d6a1635 --- /dev/null +++ b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-unknown-shell.golden @@ -0,0 +1,7 @@ +Error: unsupported shell type "fish" +Usage: + completion SHELL [flags] + +Flags: + -h, --help help for completion + diff --git a/cmd/completion/testdata/TestCompletionGoldenOutput/completion-zsh.golden b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-zsh.golden new file mode 100644 index 000000000..77193d2cd --- /dev/null +++ b/cmd/completion/testdata/TestCompletionGoldenOutput/completion-zsh.golden @@ -0,0 +1,441 @@ +#compdef airshipctl + +__airshipctl_bash_source() { + alias shopt=':' + alias _expand=_bash_expand + alias _complete=_bash_comp + emulate -L sh + setopt kshglob noshglob braceexpand + source "$@" +} +__airshipctl_type() { + # -t is not supported by zsh + if [ "$1" == "-t" ]; then + shift + # fake Bash 4 to disable "complete -o nospace". Instead + # "compopt +-o nospace" is used in the code to toggle trailing + # spaces. We don't support that, but leave trailing spaces on + # all the time + if [ "$1" = "__airshipctl_compopt" ]; then + echo builtin + return 0 + fi + fi + type "$@" +} +__airshipctl_compgen() { + local completions w + completions=( $(compgen "$@") ) || return $? + # filter by given word as prefix + while [[ "$1" = -* && "$1" != -- ]]; do + shift + shift + done + if [[ "$1" == -- ]]; then + shift + fi + for w in "${completions[@]}"; do + if [[ "${w}" = "$1"* ]]; then + echo "${w}" + fi + done +} +__airshipctl_compopt() { + true # don't do anything. Not supported by bashcompinit in zsh +} +__airshipctl_declare() { + if [ "$1" == "-F" ]; then + whence -w "$@" + else + builtin declare "$@" + fi +} +__airshipctl_ltrim_colon_completions() +{ + if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then + # Remove colon-word prefix from COMPREPLY items + local colon_word=${1%${1##*:}} + local i=${#COMPREPLY[*]} + while [[ $((--i)) -ge 0 ]]; do + COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"} + done + fi +} +__airshipctl_get_comp_words_by_ref() { + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[${COMP_CWORD}-1]}" + words=("${COMP_WORDS[@]}") + cword=("${COMP_CWORD[@]}") +} +__airshipctl_filedir() { + local RET OLD_IFS w qw + __debug "_filedir $@ cur=$cur" + if [[ "$1" = \~* ]]; then + # somehow does not work. Maybe, zsh does not call this at all + eval echo "$1" + return 0 + fi + OLD_IFS="$IFS" + IFS=$'\n' + if [ "$1" = "-d" ]; then + shift + RET=( $(compgen -d) ) + else + RET=( $(compgen -f) ) + fi + IFS="$OLD_IFS" + IFS="," __debug "RET=${RET[@]} len=${#RET[@]}" + for w in ${RET[@]}; do + if [[ ! "${w}" = "${cur}"* ]]; then + continue + fi + if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then + qw="$(__airshipctl_quote "${w}")" + if [ -d "${w}" ]; then + COMPREPLY+=("${qw}/") + else + COMPREPLY+=("${qw}") + fi + fi + done +} +__airshipctl_quote() { + if [[ $1 == \'* || $1 == \"* ]]; then + # Leave out first character + printf %q "${1:1}" + else + printf %q "$1" + fi +} +autoload -U +X bashcompinit && bashcompinit +# use word boundary patterns for BSD or GNU sed +LWORD='[[:<:]]' +RWORD='[[:>:]]' +if sed --help 2>&1 | grep -q GNU; then + LWORD='\<' + RWORD='\>' +fi +__airshipctl_convert_bash_to_zsh() { + sed \ + -e 's/declare -F/whence -w/' \ + -e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \ + -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \ + -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \ + -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \ + -e "s/${LWORD}_filedir${RWORD}/__airshipctl_filedir/g" \ + -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__airshipctl_get_comp_words_by_ref/g" \ + -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__airshipctl_ltrim_colon_completions/g" \ + -e "s/${LWORD}compgen${RWORD}/__airshipctl_compgen/g" \ + -e "s/${LWORD}compopt${RWORD}/__airshipctl_compopt/g" \ + -e "s/${LWORD}declare${RWORD}/__airshipctl_declare/g" \ + -e "s/\\\$(type${RWORD}/\$(__airshipctl_type/g" \ + -e 's/aliashash\["\(.\{1,\}\)"\]/aliashash[\1]/g' \ + -e 's/FUNCNAME/funcstack/g' \ + <<'BASH_COMPLETION_EOF' +# bash completion for completion -*- shell-script -*- + +__completion_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Homebrew on Macs have version 1.3 of bash-completion which doesn't include +# _init_completion. This is a very minimal version of that function. +__completion_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +__completion_index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 +} + +__completion_contains_word() +{ + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 +} + +__completion_handle_reply() +{ + __completion_debug "${FUNCNAME[0]}" + case $cur in + -*) + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + local allflags + if [ ${#must_have_one_flag[@]} -ne 0 ]; then + allflags=("${must_have_one_flag[@]}") + else + allflags=("${flags[*]} ${two_word_flags[*]}") + fi + COMPREPLY=( $(compgen -W "${allflags[*]}" -- "$cur") ) + if [[ $(type -t compopt) = "builtin" ]]; then + [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace + fi + + # complete after --flag=abc + if [[ $cur == *=* ]]; then + if [[ $(type -t compopt) = "builtin" ]]; then + compopt +o nospace + fi + + local index flag + flag="${cur%=*}" + __completion_index_of_word "${flag}" "${flags_with_completion[@]}" + COMPREPLY=() + if [[ ${index} -ge 0 ]]; then + PREFIX="" + cur="${cur#*=}" + ${flags_completion[${index}]} + if [ -n "${ZSH_VERSION}" ]; then + # zsh completion needs --flag= prefix + eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" + fi + fi + fi + return 0; + ;; + esac + + # check if we are handling a flag with special work handling + local index + __completion_index_of_word "${prev}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + ${flags_completion[${index}]} + return + fi + + # we are parsing a flag and don't have a special handler, no completion + if [[ ${cur} != "${words[cword]}" ]]; then + return + fi + + local completions + completions=("${commands[@]}") + if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then + completions=("${must_have_one_noun[@]}") + fi + if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then + completions+=("${must_have_one_flag[@]}") + fi + COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") ) + + if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then + COMPREPLY=( $(compgen -W "${noun_aliases[*]}" -- "$cur") ) + fi + + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + declare -F __custom_func >/dev/null && __custom_func + fi + + # available in bash-completion >= 2, not always present on macOS + if declare -F __ltrim_colon_completions >/dev/null; then + __ltrim_colon_completions "$cur" + fi + + # If there is only 1 completion and it is a flag with an = it will be completed + # but we don't want a space after the = + if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then + compopt -o nospace + fi +} + +# The arguments should be in the form "ext1|ext2|extn" +__completion_handle_filename_extension_flag() +{ + local ext="$1" + _filedir "@(${ext})" +} + +__completion_handle_subdirs_in_dir_flag() +{ + local dir="$1" + pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 +} + +__completion_handle_flag() +{ + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + # if a command required a flag, and we found it, unset must_have_one_flag() + local flagname=${words[c]} + local flagvalue + # if the word contained an = + if [[ ${words[c]} == *"="* ]]; then + flagvalue=${flagname#*=} # take in as flagvalue after the = + flagname=${flagname%=*} # strip everything after the = + flagname="${flagname}=" # but put the = back + fi + __completion_debug "${FUNCNAME[0]}: looking for ${flagname}" + if __completion_contains_word "${flagname}" "${must_have_one_flag[@]}"; then + must_have_one_flag=() + fi + + # if you set a flag which only applies to this command, don't show subcommands + if __completion_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then + commands=() + fi + + # keep flag value with flagname as flaghash + # flaghash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then + if [ -n "${flagvalue}" ] ; then + flaghash[${flagname}]=${flagvalue} + elif [ -n "${words[ $((c+1)) ]}" ] ; then + flaghash[${flagname}]=${words[ $((c+1)) ]} + else + flaghash[${flagname}]="true" # pad "true" for bool flag + fi + fi + + # skip the argument to a two word flag + if __completion_contains_word "${words[c]}" "${two_word_flags[@]}"; then + c=$((c+1)) + # if we are looking for a flags value, don't show commands + if [[ $c -eq $cword ]]; then + commands=() + fi + fi + + c=$((c+1)) + +} + +__completion_handle_noun() +{ + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + if __completion_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then + must_have_one_noun=() + elif __completion_contains_word "${words[c]}" "${noun_aliases[@]}"; then + must_have_one_noun=() + fi + + nouns+=("${words[c]}") + c=$((c+1)) +} + +__completion_handle_command() +{ + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_completion_root_command" + else + next_command="_${words[c]//:/__}" + fi + fi + c=$((c+1)) + __completion_debug "${FUNCNAME[0]}: looking for ${next_command}" + declare -F "$next_command" >/dev/null && $next_command +} + +__completion_handle_word() +{ + if [[ $c -ge $cword ]]; then + __completion_handle_reply + return + fi + __completion_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + if [[ "${words[c]}" == -* ]]; then + __completion_handle_flag + elif __completion_contains_word "${words[c]}" "${commands[@]}"; then + __completion_handle_command + elif [[ $c -eq 0 ]]; then + __completion_handle_command + elif __completion_contains_word "${words[c]}" "${command_aliases[@]}"; then + # aliashash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then + words[c]=${aliashash[${words[c]}]} + __completion_handle_command + else + __completion_handle_noun + fi + else + __completion_handle_noun + fi + __completion_handle_word +} + +_completion_root_command() +{ + last_command="completion" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--help") + flags+=("-h") + local_nonpersistent_flags+=("--help") + + must_have_one_flag=() + must_have_one_noun=() + must_have_one_noun+=("bash") + must_have_one_noun+=("zsh") + noun_aliases=() +} + +__start_completion() +{ + local cur prev words cword + declare -A flaghash 2>/dev/null || : + declare -A aliashash 2>/dev/null || : + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -s || return + else + __completion_init_completion -n "=" || return + fi + + local c=0 + local flags=() + local two_word_flags=() + local local_nonpersistent_flags=() + local flags_with_completion=() + local flags_completion=() + local commands=("completion") + local must_have_one_flag=() + local must_have_one_noun=() + local last_command + local nouns=() + + __completion_handle_word +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_completion completion +else + complete -o default -o nospace -F __start_completion completion +fi + +# ex: ts=4 sw=4 et filetype=sh + +BASH_COMPLETION_EOF +} +__airshipctl_bash_source <(__airshipctl_convert_bash_to_zsh) diff --git a/pkg/util/configreader_test.go b/pkg/util/configreader_test.go new file mode 100644 index 000000000..52cad248a --- /dev/null +++ b/pkg/util/configreader_test.go @@ -0,0 +1,22 @@ +package util_test + +import ( + "testing" + + "opendev.org/airship/airshipctl/pkg/util" +) + +func TestReadYAMLFile(t *testing.T) { + var actual map[string]interface{} + if err := util.ReadYAMLFile("testdata/test.yaml", &actual); err != nil { + t.Fatalf("Error while reading YAML: %s", err.Error()) + } + expectedString := "test" + actualString, ok := actual["testString"] + if !ok { + t.Fatalf("Missing \"testString\" attribute") + } + if actualString != expectedString { + t.Errorf("Expected %s, got %s", expectedString, actualString) + } +} diff --git a/pkg/util/testdata/test.yaml b/pkg/util/testdata/test.yaml new file mode 100644 index 000000000..cb8585cdc --- /dev/null +++ b/pkg/util/testdata/test.yaml @@ -0,0 +1 @@ +testString: test diff --git a/testutil/utilities.go b/testutil/utilities.go index 87773e0d7..c55a9faea 100644 --- a/testutil/utilities.go +++ b/testutil/utilities.go @@ -33,6 +33,9 @@ type CmdTest struct { // The instatiated version of the root airshipctl command to test Cmd *cobra.Command + + // The expected error + Error error } // RunTest either asserts that a specific command's output matches the expected @@ -47,9 +50,8 @@ func RunTest(t *testing.T, test *CmdTest) { args := strings.Fields(test.CmdLine) cmd.SetArgs(args) - if err := cmd.Execute(); err != nil { - t.Fatalf("Unexpected error: %s", err.Error()) - } + err := cmd.Execute() + checkError(t, err, test.Error) if *shouldUpdateGolden { updateGolden(t, test, actual.Bytes()) @@ -58,6 +60,20 @@ func RunTest(t *testing.T, test *CmdTest) { } } +// ReadFixtureBytes is a convenience function for opening a test fixture +func ReadFixtureBytes(t *testing.T, filename string) []byte { + fixtureData, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatalf("Unexpected error while reading fixture at %s: %s", filename, err.Error()) + } + return fixtureData +} + +// ReadFixtureString is a convenience function for opening a test fixture +func ReadFixtureString(t *testing.T, filename string) string { + return string(ReadFixtureBytes(t, filename)) +} + func updateGolden(t *testing.T, test *CmdTest, actual []byte) { goldenDir := filepath.Join(testdataDir, t.Name()+goldenDirSuffix) if err := os.MkdirAll(goldenDir, 0775); err != nil { @@ -84,6 +100,23 @@ func assertEqualGolden(t *testing.T, test *CmdTest, actual []byte) { } } +func checkError(t *testing.T, actual, expected error) { + if expected == nil { + if actual == nil { + return + } + t.Fatalf("Unexpected error: %q", actual.Error()) + } + + if actual == nil { + t.Fatalf("Expected error %q, but got nil", expected.Error()) + } + + if actual.Error() != expected.Error() { + t.Fatalf("Expected error %q, but got %q", expected.Error(), actual.Error()) + } +} + func normalize(in []byte) []byte { return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1) } diff --git a/tools/coverage_check b/tools/coverage_check index d6634a317..203fa8c20 100755 --- a/tools/coverage_check +++ b/tools/coverage_check @@ -1,5 +1,5 @@ #!/bin/bash -set -ex +set -e if [[ $# -ne 1 ]]; then printf "Usage: %s \n" "$0"