package main import ( "os" "path/filepath" "strings" "testing" ) // TestBashFunctionName verifies detection of library-style bash scripts // that define a function matching the file basename. func TestBashFunctionName(t *testing.T) { cases := []struct { name string filename string content string wantFn string }{ { name: "library-style: defines matching function", filename: "gradle_unit_test.sh", content: `#!/usr/bin/env bash SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/gradle_run.sh" gradle_unit_test() { local project_dir="$1" echo "running tests in $project_dir" } `, wantFn: "gradle_unit_test", }, { name: "library-style: defines matching function with space before parens", filename: "port_kill.sh", content: `#!/usr/bin/env bash port_kill () { echo "killing port $1" } `, wantFn: "port_kill", }, { name: "top-level pipeline: no matching function", filename: "propose_capability_groups.sh", content: `#!/usr/bin/env bash # inline pipeline logic find_root() { echo "root"; } while [[ $# -gt 0 ]]; do shift done `, wantFn: "", }, { name: "self-executing (has guard): no separate function detection needed", filename: "rsync_deploy.sh", content: `#!/usr/bin/env bash rsync_deploy() { echo "deploying" } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then rsync_deploy "$@" fi `, // bashFunctionName should still return the function name — the // caller (buildBashCommand) decides which path to take based on // the guard, not this helper. wantFn: "rsync_deploy", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := bashFunctionName("/tmp/"+tc.filename, tc.content) if got != tc.wantFn { t.Errorf("bashFunctionName(%q) = %q, want %q", tc.filename, got, tc.wantFn) } }) } } // TestBuildBashCommand_LibraryStyle verifies that a library-style bash script // (defines a function but has no self-invocation guard) is run by sourcing // the script and calling the function — producing real stdout output. func TestBuildBashCommand_LibraryStyle(t *testing.T) { dir := t.TempDir() // Write a library-style script: defines a function, no guard. scriptPath := filepath.Join(dir, "say_hello.sh") scriptContent := `#!/usr/bin/env bash say_hello() { echo "hello from $1" } ` if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { t.Fatal(err) } cmd, err := buildBashCommand(scriptPath, []string{"world"}) if err != nil { t.Fatalf("buildBashCommand error: %v", err) } out, err := cmd.Output() if err != nil { t.Fatalf("cmd.Output() error: %v", err) } got := strings.TrimSpace(string(out)) want := "hello from world" if got != want { t.Errorf("output = %q, want %q", got, want) } } // TestBuildBashCommand_SelfGuard verifies that a script with the BASH_SOURCE // guard is run directly (not via source+call) and still produces output. func TestBuildBashCommand_SelfGuard(t *testing.T) { dir := t.TempDir() scriptPath := filepath.Join(dir, "say_hi.sh") scriptContent := `#!/usr/bin/env bash say_hi() { echo "hi from $1" } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then say_hi "$@" fi ` if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { t.Fatal(err) } cmd, err := buildBashCommand(scriptPath, []string{"guard"}) if err != nil { t.Fatalf("buildBashCommand error: %v", err) } out, err := cmd.Output() if err != nil { t.Fatalf("cmd.Output() error: %v", err) } got := strings.TrimSpace(string(out)) want := "hi from guard" if got != want { t.Errorf("output = %q, want %q", got, want) } } // TestBuildBashCommand_TopLevelPipeline verifies that a pipeline script that // runs top-level code directly (no function, no guard) produces output. func TestBuildBashCommand_TopLevelPipeline(t *testing.T) { dir := t.TempDir() scriptPath := filepath.Join(dir, "my_pipeline.sh") scriptContent := `#!/usr/bin/env bash echo "pipeline ran with $# args: $@" ` if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { t.Fatal(err) } cmd, err := buildBashCommand(scriptPath, []string{"a", "b"}) if err != nil { t.Fatalf("buildBashCommand error: %v", err) } out, err := cmd.Output() if err != nil { t.Fatalf("cmd.Output() error: %v", err) } got := strings.TrimSpace(string(out)) want := "pipeline ran with 2 args: a b" if got != want { t.Errorf("output = %q, want %q", got, want) } }