Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion go-core.bash
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ declare -x _GO_CMD_NAME=
# string with the arguments delimited by the ASCII Unit Separator ($'\x1f').
declare -x _GO_CMD_ARGV=

# The directory in which plugins are installed.
# The top-level directory in which plugins are installed.
#
# If a command script is running as a plugin, this value will be the plugins
# directory of the top-level `./go` script.
declare _GO_PLUGINS_DIR=

# Directories containing executable plugin scripts.
Expand Down Expand Up @@ -204,6 +207,52 @@ declare _GO_INJECT_MODULE_PATH="$_GO_INJECT_MODULE_PATH"
return "$result"
}

# Searches through plugin directories using a helper function
#
# The search will begin in `_GO_SCRIPTS_DIR/plugins`. As long as `search_func`
# returns nonzero, every parent `/plugins/` directory will be searched, up to
# and including the top-level `_GO_PLUGINS_DIR`. The search will end either when
# `search_func` returns zero, or when all of the plugin paths are exhausted.
#
# The helper function, `search_func`, will receive the current plugin directory
# being searched as its sole argument. The `@go.search_plugins` caller's
# variables will be available in its scope. It should return zero when the
# search criteria are satisfied, nonzero if the search should continue.
#
# For example, to search for a particular item in a particular plugin:
#
# find_plugin_item() {
# [[ -e "$1/item_path" ]] && item_path="$1/item_path"
# }
#
# my_func() {
# local item_path='foo/bar'
# if @go.search_plugins find_plugin_item; then
# # Do something with $item_path
# fi
# }
#
# Arguments:
# search_func: Helper function implementing the search operation
#
# Returns:
# Zero if `search_func` ever returns zero, nonzero otherwise
@go.search_plugins() {
local __gsp_plugins_dir="$_GO_SCRIPTS_DIR/plugins"

# Set `_GO_PLUGINS_DIR` if called from a top-level `./go` script before `@go`.
_GO_PLUGINS_DIR="${_GO_PLUGINS_DIR:-$_GO_SCRIPTS_DIR/plugins}"

while true; do
if "$1" "$__gsp_plugins_dir"; then
return
elif [[ "$__gsp_plugins_dir" == "$_GO_PLUGINS_DIR" ]]; then
return 1
fi
__gsp_plugins_dir="${__gsp_plugins_dir%/plugins/*}/plugins"
done
}

# Main driver of ./go script functionality.
#
# Arguments:
Expand Down
26 changes: 10 additions & 16 deletions lib/internal/path
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
#! /bin/bash

_@go.set_search_paths_add_plugin_paths() {
local plugin_paths=("$1"/*/bin)
if [[ "${plugin_paths[0]}" != "$1/*/bin" ]]; then
_GO_PLUGINS_PATHS+=("${plugin_paths[@]}")
fi
return 1
}

_@go.set_search_paths() {
local plugins_dir="$_GO_SCRIPTS_DIR/plugins"
local plugins_paths=()
local plugin_path

if [[ -n "$_GO_INJECT_SEARCH_PATH" ]]; then
Expand All @@ -13,21 +19,9 @@ _@go.set_search_paths() {
# A plugin's own local plugin paths will appear before inherited ones. If
# there is a version incompatibility issue with other installed plugins, this
# allows a plugin's preferred version to take precedence.
while [[ "$plugins_dir" =~ ^$_GO_PLUGINS_DIR ]]; do
plugin_paths=("$plugins_dir"/*/bin)
if [[ "${plugin_paths[0]}" != "$plugins_dir/*/bin" ]]; then
_GO_PLUGINS_PATHS+=("${plugin_paths[@]}")
fi
if [[ "$plugins_dir" == "$_GO_PLUGINS_DIR" ]]; then
break
fi
plugins_dir="${plugins_dir%/plugins/*}/plugins"
done
@go.search_plugins '_@go.set_search_paths_add_plugin_paths'

# A plugin's _GO_SCRIPTS_DIR may continue to appear in _GO_PLUGINS_PATHS so
# that it's available to other plugins that depend upon it as a circular
# dependency (though such dependencies are strongly discouraged). However, we
# will ensure its _GO_SCRIPTS_DIR isn't duplicated in _GO_SEARCH_PATHS.
# Ensure a plugin's _GO_SCRIPTS_DIR isn't duplicated in _GO_SEARCH_PATHS.
for plugin_path in "${_GO_PLUGINS_PATHS[@]}"; do
if [[ "$plugin_path" != "$_GO_SCRIPTS_DIR" ]]; then
_GO_SEARCH_PATHS+=("$plugin_path")
Expand Down
152 changes: 152 additions & 0 deletions tests/core/search-plugins.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#! /usr/bin/env bats

load ../environment

setup() {
test_filter
@go.create_test_go_script \
'collect_dirs_impl() {' \
' [[ -d "$1" ]] && dirs_searched+=("$1")' \
' [[ "$((--COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS))" -eq "0" ]]' \
'}' \
'collect_dirs() {' \
' local dirs_searched=()' \
' @go.search_plugins collect_dirs_impl' \
' local result="$?"' \
' printf "%s\n" "${dirs_searched[@]}"' \
' return "$result"' \
'}' \
'if [[ -z "$COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS" ]]; then' \
' COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS=1' \
'fi' \
'@go "$@"'
}

teardown() {
@go.remove_test_go_rootdir
}

@test "$SUITE: _GO_PLUGINS_DIR doesn't exist" {
@go.create_test_command_script 'top-level' 'collect_dirs'
run "$TEST_GO_SCRIPT" 'top-level'
assert_success ''
}

@test "$SUITE: _GO_PLUGINS_DIR exists" {
mkdir -p "$TEST_GO_PLUGINS_DIR"
@go.create_test_command_script 'top-level' 'collect_dirs'
run "$TEST_GO_SCRIPT" 'top-level'
assert_success "$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: _GO_PLUGINS_DIR exists, search returns failure" {
mkdir -p "$TEST_GO_PLUGINS_DIR"
@go.create_test_command_script 'top' 'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='0' run "$TEST_GO_SCRIPT" 'top'
assert_failure "$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: plugin without plugins dir finds _GO_PLUGINS_DIR" {
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='2' run "$TEST_GO_SCRIPT" 'foo'
assert_success "$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: plugin with plugins dir finds both plugins dirs" {
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='2' run "$TEST_GO_SCRIPT" 'foo'
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
"$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: plugin with plugins dir, return success after first dir" {
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='1' run "$TEST_GO_SCRIPT" 'foo'
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
}

@test "$SUITE: plugin finds both plugins dirs, returns failure" {
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
@go.create_test_command_script 'plugins/foo/bin/foo' 'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='3' run "$TEST_GO_SCRIPT" 'foo'
assert_failure "$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
"$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: nested_plugin without plugins dir finds parent dirs" {
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin"
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='3' run "$TEST_GO_SCRIPT" 'foo'
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
"$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: nested_plugin with plugins dir finds all plugin dirs" {
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins"
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='3' run "$TEST_GO_SCRIPT" 'foo'
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins" \
"$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
"$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: nested_plugin stops after parent plugin dir" {
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='2' run "$TEST_GO_SCRIPT" 'foo'
# Note it doesn't have its own plugin dir this time.
assert_success "$TEST_GO_PLUGINS_DIR/foo/bin/plugins"
}

@test "$SUITE: nested_plugin with plugins dir finds all dirs, returns failure" {
mkdir -p "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins"
@go.create_test_command_script 'plugins/foo/bin/foo' '@go bar'
@go.create_test_command_script 'plugins/foo/bin/plugins/bar/bin/bar' \
'collect_dirs'
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='4' run "$TEST_GO_SCRIPT" 'foo'
assert_failure "$TEST_GO_PLUGINS_DIR/foo/bin/plugins/bar/bin/plugins" \
"$TEST_GO_PLUGINS_DIR/foo/bin/plugins" \
"$TEST_GO_PLUGINS_DIR"
}

@test "$SUITE: /plugins/ in _GO_ROOTDIR, _GO_SCRIPTS_DIR (pathological)" {
local test_rootdir="$TEST_GO_ROOTDIR/plugins/plugins"
local test_go_script="$test_rootdir/go"
local test_scripts_dir="$test_rootdir/plugins"
local test_plugins_dir="$test_scripts_dir/plugins"
mkdir -p "$test_plugins_dir/foo/bin/plugins/bar/bin/plugins"

local line
while IFS= read -r line; do
line="${line%$'\r'}"
if [[ "$line" =~ go-core\.bash ]]; then
line=". '$_GO_CORE_DIR/go-core.bash' 'plugins'"
fi
printf '%s\n' "$line" >> "$test_go_script"
done < "$TEST_GO_SCRIPT"
chmod 700 "$test_go_script"

local foo_path="${test_plugins_dir#$BATS_TEST_ROOTDIR/}/foo/bin/foo"
local bar_path="${foo_path%/*}/plugins/bar/bin/bar"

# We can't use `@go.create_test_command_script` since we can't change the
# readonly `TEST_GO_*` variables.
create_bats_test_script "$foo_path" '@go bar'
create_bats_test_script "$bar_path" 'collect_dirs'

test_printf 'test_go_script: %s\n' "$test_go_script" >&2
test_printf 'test_plugins_dir: %s\n' "$test_plugins_dir" >&2
test_printf 'foo_path: %s\n' "${foo_path}" >&2
test_printf 'bar_path: %s\n' "${bar_path}" >&2
COLLECT_DIRS_SUCCESS_AFTER_NUM_ITERATIONS='10' run "$test_go_script" 'foo'
assert_failure "$test_plugins_dir/foo/bin/plugins/bar/bin/plugins" \
"$test_plugins_dir/foo/bin/plugins" \
"$test_plugins_dir"
}