From 8563f4de3770e871145699cbae5aa1fbe8610287 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Tue, 13 Dec 2016 18:51:15 -0500 Subject: [PATCH 1/2] core: Add @go.print_stack_trace to public API The primary use case is to provide more helpful error messages from `. "$_GO_USE_MODULES"`, but it seems generally useful enough to provide it as part of the public API. --- CONTRIBUTING.md | 2 + README.md | 6 +-- go-core.bash | 14 +++++++ tests/core/print-stack-trace.bats | 70 +++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/core/print-stack-trace.bats diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81f2ebf..b7a3c20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -310,6 +310,8 @@ it easier to find, count, and possibly transform things. - Use `@go.printf` for most console output to ensure that the text fits the terminal width. +- Use `@go.print_stack_trace` to provide a detailed error message as + appropriate, usually before calling `exit 1`. ### Gotchas diff --git a/README.md b/README.md index e078c83..ac2357e 100644 --- a/README.md +++ b/README.md @@ -323,9 +323,9 @@ Any script in any language can invoke other command scripts by running `./go [args..]`. In Bash, however, you can also invoke the `@go` function directly as `@go [args...]`. -The `@go` and `@go.printf` functions are available to command scripts written in -Bash, as Bash command scripts are sourced rather than run using another language -interpreter. +The `@go`, `@go.printf`, and `@go.print_stack_trace` functions are available to +command scripts written in Bash, as Bash command scripts are sourced rather than +run using another language interpreter. A number of global variables defined and documented in `go-core.bash`, all starting with the prefix `_GO_`, are exported as environment variables and diff --git a/go-core.bash b/go-core.bash index 044f7bc..a0a3371 100755 --- a/go-core.bash +++ b/go-core.bash @@ -139,6 +139,20 @@ declare _GO_SEARCH_PATHS=("$_GO_CORE_DIR/libexec") fi } +# Prints the stack trace at the point of the call. +# +# Arguments: +# omit_caller: If set, this function's caller is removed from the output +@go.print_stack_trace() { + local start_index="${1:+2}" + local i + + for ((i=${start_index:-1}; i != ${#FUNCNAME[@]}; ++i)); do + @go.printf ' %s:%s %s\n' "${BASH_SOURCE[$i]}" "${BASH_LINENO[$((i-1))]}" \ + "${FUNCNAME[$i]}" + done +} + # Main driver of ./go script functionality. # # Arguments: diff --git a/tests/core/print-stack-trace.bats b/tests/core/print-stack-trace.bats new file mode 100644 index 0000000..78e19e2 --- /dev/null +++ b/tests/core/print-stack-trace.bats @@ -0,0 +1,70 @@ +#! /usr/bin/env bats + +load ../environment + +teardown() { + remove_test_go_rootdir +} + +@test "$SUITE: stack trace from top level of main ./go script" { + create_test_go_script '@go.print_stack_trace' + run "$TEST_GO_SCRIPT" + assert_success " $TEST_GO_SCRIPT:3 main" +} + +@test "$SUITE: stack trace from top level of main ./go script without caller" { + create_test_go_script '@go.print_stack_trace omit_caller' + run "$TEST_GO_SCRIPT" + assert_success '' +} + +@test "$SUITE: stack trace from function inside main ./go script" { + create_test_go_script \ + 'print_stack() {' \ + ' @go.print_stack_trace' \ + '}' \ + 'print_stack' + run "$TEST_GO_SCRIPT" + + local expected=(" $TEST_GO_SCRIPT:4 print_stack" + " $TEST_GO_SCRIPT:6 main") + local IFS=$'\n' + assert_success "${expected[*]}" +} + +@test "$SUITE: omit function caller from stack trace" { + create_test_go_script \ + 'print_stack() {' \ + " @go.print_stack_trace omit_caller" \ + '}' \ + 'print_stack' + run "$TEST_GO_SCRIPT" + assert_success " $TEST_GO_SCRIPT:6 main" +} + +@test "$SUITE: stack trace from subcommand script" { + create_test_go_script '@go "$@"' + create_test_command_script 'foo' \ + 'foo_func() {' \ + ' @go foo bar' \ + '}' \ + 'foo_func' + create_test_command_script 'foo.d/bar' \ + 'bar_func() {' \ + ' @go.print_stack_trace omit_caller' \ + '}' \ + 'bar_func' + + run "$TEST_GO_SCRIPT" foo + + local go_core_pattern="$_GO_CORE_DIR/go-core.bash:[0-9]+" + assert_success + assert_line_equals 0 " $TEST_GO_SCRIPTS_DIR/foo.d/bar:5 source" + assert_line_matches 1 " $go_core_pattern _@go.run_command_script" + assert_line_matches 2 " $go_core_pattern @go" + assert_line_equals 3 " $TEST_GO_SCRIPTS_DIR/foo:3 foo_func" + assert_line_equals 4 " $TEST_GO_SCRIPTS_DIR/foo:5 source" + assert_line_matches 5 " $go_core_pattern _@go.run_command_script" + assert_line_matches 6 " $go_core_pattern @go" + assert_line_equals 7 " $TEST_GO_SCRIPT:3 main" +} From 30790c963d6c5671b44f23f0e553cbefdc17ca9d Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Tue, 13 Dec 2016 17:13:25 -0500 Subject: [PATCH 2/2] use: Show stack trace when an import fails --- lib/internal/use | 7 +++++-- tests/modules/use.bats | 33 +++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/internal/use b/lib/internal/use index 6c774fb..6ada2fb 100644 --- a/lib/internal/use +++ b/lib/internal/use @@ -90,14 +90,17 @@ for __go_module_name in "$@"; do __go_module_file="$_GO_SCRIPTS_DIR/lib/$__go_module_name" if [[ ! -f "$__go_module_file" ]]; then - @go.printf "ERROR: Unknown module: $__go_module_name\n" >&2 + @go.printf 'ERROR: Module %s not found at:\n' "$__go_module_name" >&2 + @go.print_stack_trace omit_caller >&2 exit 1 fi fi fi if ! . "$__go_module_file"; then - @go.printf "ERROR: Module import failed for: $__go_module_file\n" >&2 + @go.printf 'ERROR: Failed to import %s module from %s at:\n' \ + "$__go_module_name" "$__go_module_file" >&2 + @go.print_stack_trace omit_caller >&2 exit 1 fi done diff --git a/tests/modules/use.bats b/tests/modules/use.bats index f568d3c..cef5d25 100644 --- a/tests/modules/use.bats +++ b/tests/modules/use.bats @@ -45,7 +45,11 @@ teardown() { @test "$SUITE: error if nonexistent module specified" { run "$TEST_GO_SCRIPT" 'bogus-test-module' - assert_failure 'ERROR: Unknown module: bogus-test-module' + + local expected=('ERROR: Module bogus-test-module not found at:' + " $TEST_GO_SCRIPT:3 main") + local IFS=$'\n' + assert_failure "${expected[*]}" } @test "$SUITE: import modules successfully" { @@ -73,12 +77,33 @@ teardown() { } @test "$SUITE: error if module contains errors" { - echo "This is a totally broken module." >> "${TEST_MODULES[1]}" + local module="${IMPORTS[1]}" + local module_file="${TEST_MODULES[2]}" + + echo "This is a totally broken module." > "$module_file" + run "$TEST_GO_SCRIPT" "${IMPORTS[@]}" + + local expected=("${IMPORTS[0]##*/} loaded" + "$module_file: line 1: This: command not found" + "ERROR: Failed to import $module module from $module_file at:" + " $TEST_GO_SCRIPT:3 main") + local IFS=$'\n' + assert_failure "${expected[*]}" +} + +@test "$SUITE: error if module returns an error" { + local module="${IMPORTS[1]}" + local module_file="${TEST_MODULES[2]}" + local error_message='These violent delights have violent ends...' + + echo "echo '$error_message' >&2" > "$module_file" + echo "return 1" >> "$module_file" run "$TEST_GO_SCRIPT" "${IMPORTS[@]}" local expected=("${IMPORTS[0]##*/} loaded" - "${TEST_MODULES[1]}: line 2: This: command not found" - "ERROR: Module import failed for: ${TEST_MODULES[1]}") + "$error_message" + "ERROR: Failed to import $module module from $module_file at:" + " $TEST_GO_SCRIPT:3 main") local IFS=$'\n' assert_failure "${expected[*]}" }