diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..1a39c1c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,35 @@ +name: πŸ§ͺ Code Coverage & Testing + +on: + push: + branches: + - 'main' + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.gitignore' + - '.goreleaser.yaml' + - 'example/**' + - 'docs/**' + pull_request: + branches: + - 'main' + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.gitignore' + - '.goreleaser.yaml' + - 'example/**' + - 'docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: πŸ›‘ Testing Suite + uses: ./.github/workflows/test-suite.yaml + with: + use-docker-sidecar: true # 🐳 Enable full testing environment + use-sidecar-remote-share: true # πŸ“ Enable remote file sharing for testing diff --git a/.github/workflows/debug.yaml b/.github/workflows/debug.yaml new file mode 100644 index 0000000..24bb48a --- /dev/null +++ b/.github/workflows/debug.yaml @@ -0,0 +1,288 @@ +# πŸš€ Remote Debug Workflow - SSH Tunnel Edition +# =============================================== +# This workflow creates a secure SSH tunnel for remote debugging sessions +# +# ✨ Features: +# β€’ πŸ” Secure SSH access to build environments +# β€’ πŸ› Pre-installed Go debugger (Delve) +# β€’ 🌍 Multi-platform support (Linux, macOS, Windows) +# β€’ πŸ”„ Port forwarding for seamless debug sessions +# β€’ 🐳 Docker sidecar for enhanced compatibility +# β€’ πŸ“ Persistent Go environment configuration + +name: 🚧 Debug with SSH + +on: + workflow_dispatch: + inputs: + os: + description: 'πŸ–₯️ Select your debugging platform' + required: false + type: choice + options: + - '🐧 Ubuntu LTS (amd64)' + - '🐧 Ubuntu LTS (arm64)' + - '🍎 macOS Latest (arm64)' + - '🍎 macOS 13 (amd64)' + - 'πŸͺŸ Windows Latest (amd64)' + - 'πŸͺŸ Windows Latest (arm64)' + +jobs: + select-os: + name: πŸ—ΊοΈ Platform Selection β†’ ${{ inputs.os }} + runs-on: ubuntu-latest + outputs: + runner-os: ${{ steps.map-os.outputs.runner-os }} + os-type: ${{ steps.map-os.outputs.os-type }} + use-docker-sidecar: ${{ steps.map-os.outputs.use-docker-sidecar }} + private-key: ${{ steps.generate-key.outputs.private-key }} + public-key: ${{ steps.generate-key.outputs.public-key }} + steps: + - name: 🎯 Map OS selection to runner + id: map-os + env: + INPUT_OS: ${{ inputs.os }} + run: | + echo "πŸ” Mapping OS selection: $INPUT_OS" + case "$INPUT_OS" in + *"Ubuntu LTS (amd64)"*) + echo "βœ… Selected: Ubuntu Latest (AMD64)" + echo "runner-os=ubuntu-latest" >> $GITHUB_OUTPUT + echo "os-type=linux" >> $GITHUB_OUTPUT + echo "use-docker-sidecar=false" >> $GITHUB_OUTPUT + ;; + *"Ubuntu LTS (arm64)"*) + echo "βœ… Selected: Ubuntu 24.04 (ARM64)" + echo "runner-os=ubuntu-24.04-arm" >> $GITHUB_OUTPUT + echo "os-type=linux" >> $GITHUB_OUTPUT + echo "use-docker-sidecar=false" >> $GITHUB_OUTPUT + ;; + *"macOS Latest (arm64)"*) + echo "βœ… Selected: macOS Latest (ARM64)" + echo "runner-os=macos-latest" >> $GITHUB_OUTPUT + echo "os-type=macos" >> $GITHUB_OUTPUT + echo "use-docker-sidecar=true" >> $GITHUB_OUTPUT + ;; + *"macOS 13 (amd64)"*) + echo "βœ… Selected: macOS 13 (AMD64)" + echo "runner-os=macos-13" >> $GITHUB_OUTPUT + echo "os-type=macos" >> $GITHUB_OUTPUT + echo "use-docker-sidecar=false" >> $GITHUB_OUTPUT + ;; + *"Windows Latest (amd64)"*) + echo "βœ… Selected: Windows Latest (AMD64)" + echo "runner-os=windows-latest" >> $GITHUB_OUTPUT + echo "os-type=windows" >> $GITHUB_OUTPUT + echo "use-docker-sidecar=false" >> $GITHUB_OUTPUT + ;; + *"Windows Latest (arm64)"*) + echo "βœ… Selected: Windows 11 (ARM64)" + echo "runner-os=windows-11-arm" >> $GITHUB_OUTPUT + echo "os-type=windows" >> $GITHUB_OUTPUT + echo "use-docker-sidecar=true" >> $GITHUB_OUTPUT + ;; + *) + echo "⚠️ No specific OS selected, defaulting to Ubuntu Latest" + echo "runner-os=ubuntu-latest" >> $GITHUB_OUTPUT + echo "os-type=linux" >> $GITHUB_OUTPUT + echo "use-docker-sidecar=false" >> $GITHUB_OUTPUT + ;; + esac + + - name: πŸ”‘ Generate SSH Key Pair (ED25519) + id: generate-key + run: | + echo "πŸ” Generating secure SSH key pair..." + ssh-keygen -t ed25519 -N "" -f ./id_ed25519 + + echo "πŸ“€ Exporting private SSH key" + { + echo "private-key<> "$GITHUB_OUTPUT" + + echo "πŸ“€ Exporting public SSH key" + echo "public-key=$(cat ./id_ed25519.pub)" >> $GITHUB_OUTPUT + echo "βœ… SSH key pair generated successfully!" + + linux-sidecar: + name: 🐳 Linux Docker Sidecar + needs: [select-os] + runs-on: ubuntu-latest + if: needs.select-os.outputs.use-docker-sidecar == 'true' + steps: + - name: πŸš€ Launch Linux Docker sidecar + uses: lexbritvin/docker-sidecar-action/run-sidecar@main + with: + ssh-server-authorized-keys: ${{ needs.select-os.outputs.public-key }} + use-bore: 'true' + + - name: ⏳ Wait for debug session to complete + uses: lexbritvin/wait-action@v1 + with: + condition-type: 'job' + job-name: '/How to connect/' + timeout-seconds: 3600 + poll-interval-seconds: 30 + + debug-session: + name: πŸ‘‰ How to connect πŸ‘ˆ + needs: select-os + runs-on: ${{ needs.select-os.outputs.runner-os || 'ubuntu-latest' }} + steps: + - name: πŸ“₯ Checkout repository + uses: actions/checkout@v4 + + - name: πŸ’» System Information + uses: lexbritvin/os-info-action@v1 + + - name: 🐹 Set up Go environment + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: πŸ› οΈ Install debugging tools + shell: bash + run: | + echo "πŸ”§ Setting up debugging environment..." + + echo "πŸ“ Configure Go temporary directory" + export GOTMPDIR="$(pwd)/.gotmp" + mkdir -p $GOTMPDIR + echo "πŸ“ Using temp directory: $GOTMPDIR" + echo "GOTMPDIR=$GOTMPDIR" >> $GITHUB_ENV + + if [[ "${{ runner.os }}" == "macOS" ]]; then + echo "🍎 Configuring Go binary path for macOS..." + ln -s $(which go) /usr/local/bin/go + echo "βœ… Go binary linked to user path" + fi + + # Check Windows ARM64 compatibility + if [[ "${{ runner.os }}" == "Windows" && "${{ runner.arch }}" == "ARM64" ]]; then + echo "⚠️ WARNING: Delve debugger is not available on Windows ARM64" + echo "🚫 Remote debugging features will be limited on this platform" + else + echo "πŸ“¦ Installing Delve debugger..." + go install github.com/go-delve/delve/cmd/dlv@latest + echo "βœ… Delve installed successfully!" + fi + + echo "πŸ“¦ Downloading Go modules..." + go mod download + go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest + echo "βœ… Dependencies ready!" + + - name: πŸ’Ύ Save Go environment variables + shell: bash + run: | + echo "πŸ’Ύ Persisting Go environment configuration..." + echo "πŸ“‹ Saving: GOTMPDIR=${GOTMPDIR}" + + case "$RUNNER_OS" in + "Linux"|"macOS") + echo "🐧🍎 Configuring Unix-like environment..." + # Add Go environment variables to system profile + echo "export GOTMPDIR=\"${GOTMPDIR}\"" | sudo tee -a /etc/profile + echo "βœ… Environment saved to /etc/profile" + echo "πŸ’‘ To use: source /etc/profile" + ;; + + "Windows") + echo "πŸͺŸ Configuring Windows environment..." + # πŸ”§ Set system environment variables + setx GOTMPDIR "${GOTMPDIR}" //M 2>/dev/null + echo "βœ… Environment saved to Windows system variables" + echo "πŸ’‘ To use: Open new terminal session" + ;; + + *) + echo "❌ Unsupported OS: $RUNNER_OS" + exit 1 + ;; + esac + + - name: 🐳 Install Local Docker + uses: lexbritvin/setup-docker-action@main + if: needs.select-os.outputs.use-docker-sidecar == 'false' && needs.select-os.outputs.os-type != 'linux' + + - name: 🐳 Set up Remote Docker + id: docker-setup + uses: lexbritvin/docker-sidecar-action/setup-remote-docker@main + if: needs.select-os.outputs.use-docker-sidecar == 'true' + with: + private-key: ${{ needs.select-os.outputs.private-key }} + use-remote-share: 'true' + + - name: πŸ”— Establish SSH Debug Session + id: ssh-session + uses: lexbritvin/ssh-session-action@v1 + with: + use-bore: 'true' + use-actor-ssh-keys: 'true' + detached: 'true' + wait-timeout: '3600' + + - name: πŸ‘‰ How to connect πŸ‘ˆ + env: + HELP_MESSAGE: ${{ steps.ssh-session.outputs.help-message }} + EXTRA_HELP: | + ╔══════════════════════════════════════════════════════════════════════════════════════════╗ + πŸ› GO DEBUGGING WITH DELVE πŸš€ + β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• + + \033[1;36mβ”Œβ”€ πŸ”„ PORT FORWARDING SETUP\033[0m + \033[1;36mβ”‚\033[0m \033[1mFor Delve debugging, forward port 2345:\033[0m + \033[1;36mβ”‚\033[0m \033[1;33mssh -L 2345:localhost:2345 [...]\033[0m + \033[1;36m└─\033[0m + + \033[1;32mβ”Œβ”€ πŸ§ͺ TESTING COMMANDS\033[0m + \033[1;32mβ”‚\033[0m \033[1mβ€’ Run all tests:\033[0m + \033[1;32mβ”‚\033[0m \033[1;96mgo test -v ./...\033[0m + \033[1;32mβ”‚\033[0m \033[1mβ€’ Run specific test:\033[0m + \033[1;32mβ”‚\033[0m \033[1;96mgo test -v -run TestFunctionName ./...\033[0m + \033[1;32mβ”‚\033[0m \033[1mβ€’ Run with coverage:\033[0m + \033[1;32mβ”‚\033[0m \033[1;96mgo test -v -cover ./...\033[0m + \033[1;32m└─\033[0m + + \033[1;33mβ”Œβ”€ πŸ› DEBUGGING COMMANDS\033[0m + \033[1;33mβ”‚\033[0m \033[1mβ€’ Debug specific test:\033[0m + \033[1;33mβ”‚\033[0m \033[1;95mdlv --listen=:2345 --headless --api-version=2 test ./... -- -test.run TestName\033[0m + \033[1;33mβ”‚\033[0m \033[1mβ€’ Debug main application:\033[0m + \033[1;33mβ”‚\033[0m \033[1;95mdlv debug --headless --listen=:2345 --api-version=2 ./cmd/main -- [args...]\033[0m + \033[1;33mβ”‚\033[0m \033[1mβ€’ Attach to running process:\033[0m + \033[1;33mβ”‚\033[0m \033[1;95mdlv attach --headless --listen=:2345 --api-version=2 [PID]\033[0m + \033[1;33m└─\033[0m + + \033[1;34mβ”Œβ”€ πŸ”— IDE INTEGRATION\033[0m + \033[1;34mβ”‚\033[0m \033[1mβ€’ GoLand/IntelliJ IDEA:\033[0m + \033[1;34mβ”‚\033[0m \033[1mβ†’ Run/Debug Configurations β†’ Go Remote\033[0m + \033[1;34mβ”‚\033[0m \033[1mβ†’ Host: localhost, Port: 2345\033[0m + \033[1;34mβ”‚\033[0m \033[1mβ€’ VS Code:\033[0m + \033[1;34mβ”‚\033[0m \033[1mβ†’ Use 'Go: Connect to server' command\033[0m + \033[1;34mβ”‚\033[0m \033[1mβ†’ Configure launch.json with 'connect' mode\033[0m + \033[1;34m└─\033[0m + + \033[1;35mβ”Œβ”€ πŸ’‘ HELPFUL TIPS\033[0m + \033[1;35mβ”‚\033[0m \033[1mβ€’ Set breakpoints before starting debug session\033[0m + \033[1;35mβ”‚\033[0m \033[1mβ€’ Use 'dlv help' for more commands\033[0m + \033[1;35mβ”‚\033[0m \033[1mβ€’ Check firewall settings if connection fails\033[0m + \033[1;35mβ”‚\033[0m \033[1mβ€’ Session will auto-terminate after 30 minutes\033[0m + \033[1;35m└─\033[0m + + \033[1;36mπŸ“š Resources:\033[0m + \033[1mβ€’ Delve Documentation: https://github.com/go-delve/delve/tree/master/Documentation\033[0m + \033[1mβ€’ GoLand Remote Debug: https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html\033[0m + \033[1mβ€’ VS Code Go Debug: https://github.com/golang/vscode-go/blob/master/docs/debugging.md\033[0m + shell: bash + run: | + echo "πŸŽ‰ SSH Debug Session Started Successfully!" + + # Display the SSH connection instructions with enhanced formatting + printf "%b\n" "$HELP_MESSAGE" + + # Display the debugging guide with colors + printf "%b\n" "$EXTRA_HELP" + + echo "🎯 Happy debugging! Your session is ready to use." diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml new file mode 100644 index 0000000..e735bdf --- /dev/null +++ b/.github/workflows/test-suite.yaml @@ -0,0 +1,164 @@ +name: πŸ“¦ [Reusable] Multi-Platform Test Suite +on: + workflow_call: + inputs: + use-docker-sidecar: + description: '🐳 Enable Docker sidecar for remote testing environments' + required: false + type: boolean + default: false + use-sidecar-remote-share: + description: 'πŸ“ Enable remote file sharing in Docker sidecar' + required: false + type: boolean + default: false + +jobs: + client-ssh-key: + name: πŸ”‘ Generate SSH Key + runs-on: ubuntu-latest + outputs: + private-key: ${{ steps.generate-key.outputs.private-key }} + public-key: ${{ steps.generate-key.outputs.public-key }} + steps: + - name: πŸ” Generate ED25519 SSH key pair + id: generate-key + run: | + echo "πŸ”‘ Generating SSH key pair..." + ssh-keygen -t ed25519 -N "" -f ./id_ed25519 + + echo "πŸ“€ Exporting private SSH key" + { + echo "private-key<> "$GITHUB_OUTPUT" + + echo "public-key=$(cat ./id_ed25519.pub)" >> $GITHUB_OUTPUT + echo "βœ… SSH key pair generated successfully!" + + linux-sidecar: + name: 🐳 Linux Docker Sidecar + needs: [ client-ssh-key ] + runs-on: ubuntu-latest + if: ${{ inputs.use-docker-sidecar }} + steps: + - name: πŸš€ Run Linux Docker sidecar + uses: lexbritvin/docker-sidecar-action/run-sidecar@main + with: + ssh-server-authorized-keys: ${{ needs.client-ssh-key.outputs.public-key }} + use-bore: 'true' + + - name: ⏳ Wait for related jobs + uses: lexbritvin/wait-action@v1 + with: + condition-type: 'job' + job-name: '/\(sidecar\)$/' + timeout-seconds: 1800 + poll-interval-seconds: 30 + + test: + name: πŸ§ͺ Test ${{ matrix.name }} ${{ matrix.needs-sidecar && '(sidecar)' }} + strategy: + matrix: + include: + - name: 🐧 Linux (amd64) + os: ubuntu-latest + - name: 🐧 Linux (arm64) + os: ubuntu-24.04-arm + - name: 🍎 MacOS (amd64) + os: macos-13 + - name: 🍎 MacOS (arm64) + os: macos-latest + needs-sidecar: true + - name: πŸ–₯️ Windows (amd64) + os: windows-latest + - name: πŸ–₯️ Windows (arm64) + os: windows-11-arm + needs-sidecar: true + runs-on: ${{ matrix.os }} + needs: [ client-ssh-key ] + continue-on-error: ${{ matrix.continue-on-error || false }} + defaults: + run: + shell: bash + steps: + - name: πŸ“₯ Checkout code + uses: actions/checkout@v4 + + - name: πŸ“Š OS Info + uses: lexbritvin/os-info-action@v1 + + - name: πŸ—οΈ Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: πŸ“¦ Prepare dependencies + run: | + echo "πŸ“₯ Downloading Go modules..." + go mod download + go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest + echo "βœ… Dependencies downloaded successfully!" + + - name: 🐳 Install Local Docker + uses: lexbritvin/setup-docker-action@main + if: ${{ !matrix.needs-sidecar && runner.os != 'Linux' }} + + # Set up the Docker sidecar environment + - name: 🐳 Set up Remote Docker + id: docker-setup + uses: lexbritvin/docker-sidecar-action/setup-remote-docker@main + if: ${{ matrix.needs-sidecar && inputs.use-docker-sidecar }} + with: + private-key: ${{ needs.client-ssh-key.outputs.private-key }} + use-remote-share: ${{ inputs.use-sidecar-remote-share }} + + - name: πŸ§ͺ Go Test + id: go-tests + run: | + echo "πŸš€ Starting Go tests..." + export GOTMPDIR="$(pwd)/.gotmp" + mkdir -p $GOTMPDIR + echo "πŸ“ Using temp directory: $GOTMPDIR" + + echo "Make sure the binary can be built, warm up build cache..." + make build + + echo "πŸ” Running tests..." + set -euo pipefail + go test -json -v ./... 2>&1 | tee .gotmp/gotest.log | gotestfmt -hide all + + echo "βœ… All tests completed successfully!" + + # Upload the original go test log as an artifact for later review. + - name: ⬆️ Upload test log + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-log-${{ matrix.os }} + path: .gotmp/gotest.log + retention-days: 3 + if-no-files-found: error + + lint: + name: 🧹 Lint & Code Quality + runs-on: ubuntu-latest + steps: + - name: πŸ“₯ Checkout code + uses: actions/checkout@v4 + + - name: πŸ—οΈ Set up Go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: πŸ” Lint Code + run: | + echo "πŸ”§ Installing linter..." + make .install-lint + + echo "🧹 Running code linting..." + bin/golangci-lint run --timeout=5m ./... + + echo "βœ… Code linting completed successfully!" diff --git a/app_test.go b/app_test.go new file mode 100644 index 0000000..dbcd498 --- /dev/null +++ b/app_test.go @@ -0,0 +1,184 @@ +package launchr + +import ( + "os" + "os/exec" + "runtime" + "slices" + "strings" + "testing" + "time" + + "github.com/rogpeppe/go-internal/testscript" + + "github.com/launchrctl/launchr/internal/launchr" + coretest "github.com/launchrctl/launchr/test" +) + +func TestMain(m *testing.M) { + // Set testscript version. + version = "v0.0.0-testscript" + builtWith = "testscript v0.0.0" + testscript.Main(m, map[string]func(){ + "launchr": RunAndExit, + "testapp": func() { + // Set global application name. + name = "testapp" + RunAndExit() + }, + }) +} + +func TestBinary(t *testing.T) { + t.Parallel() + + type tsSetupFn = func(*testscript.Env) error + type reqFn = func(t *testing.T) + type testcase struct { + name string + dir string + files []string + setup []tsSetupFn + req []reqFn + conseq bool + } + + supportedOS := func(os ...string) reqFn { + return func(t *testing.T) { + if !slices.Contains(os, runtime.GOOS) { + t.Skipf("skipping %q, supported os: %s", runtime.GOOS, strings.Join(os, ", ")) + } + } + } + skipShort := func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + } + hasWSL := func(t *testing.T) { + if !isWSLAvailable() { + t.Skip("skipping test on Windows without WSL") + } + } + + testcases := []testcase{ + {name: "common", dir: "test/testdata/common"}, + {name: "action/discovery", dir: "test/testdata/action/discovery"}, + {name: "action/input", dir: "test/testdata/action/input"}, + + // Runtime Shell on Unix os. + { + name: "runtime/shell/unix", + dir: "test/testdata/runtime/shell", + req: []reqFn{supportedOS("linux", "darwin")}, + }, + // Runtime Shell on Windows MSYS. + // To test this on Windows, make sure that the MSYS-like bash is the first in the PATH. + { + name: "runtime/shell/win-msys", + dir: "test/testdata/runtime/shell", + req: []reqFn{supportedOS("windows")}, + setup: []tsSetupFn{coretest.SetupWorkDirUnixWin}, + }, + // Runtime Shell using Windows WSL. + // To test this on Windows, make sure that the WSL bash is the first in the PATH. + { + name: "runtime/shell/win-wsl", + dir: "test/testdata/runtime/shell", + req: []reqFn{supportedOS("windows"), hasWSL}, + setup: []tsSetupFn{coretest.SetupWSL(t)}, + }, + + // Runtime Docker. + { + name: "runtime/container/docker", + dir: "test/testdata/runtime/container", + setup: []tsSetupFn{coretest.SetupEnvDocker, coretest.SetupEnvRandom}, + req: []reqFn{skipShort}, + }, + + // Test binary build using self. + // This test must run last and should not be parallelized + // so that the build cache is warm after it. + // If it fails due to a timeout, try warming the cache manually with `make build`. + { + // Run the build once to warm up the build cache. + name: "build-warmup", + files: []string{"test/testdata/build/no-cache.txtar"}, + setup: []tsSetupFn{setupBuildEnv}, + req: []reqFn{skipShort, supportedOS("linux", "darwin")}, + conseq: true, + }, + { + name: "build", + dir: "test/testdata/build", + setup: []tsSetupFn{setupBuildEnv}, + req: []reqFn{skipShort, supportedOS("linux", "darwin")}, + }, + } + for _, tt := range testcases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + for _, fn := range tt.req { + fn(t) + } + t.TempDir() + if !tt.conseq { + t.Parallel() + } + var deadline time.Time + if !launchr.Version().Debug { + deadline = time.Now().Add(5 * time.Minute) + } + + testscript.Run(t, testscript.Params{ + Dir: tt.dir, + Files: tt.files, + Cmds: coretest.CmdsTestScript(), + Deadline: deadline, + + RequireExplicitExec: true, + RequireUniqueNames: true, + + Setup: func(env *testscript.Env) error { + for _, fn := range tt.setup { + if err := fn(env); err != nil { + return err + } + } + return nil + }, + }) + }) + } +} + +func setupBuildEnv(env *testscript.Env) error { + repoPath := MustAbs("./") + home, err := os.UserHomeDir() + if err != nil { + return err + } + env.Vars = append( + env.Vars, + "REPO_PATH="+repoPath, + "CORE_PKG="+PkgPath, + "REAL_HOME="+home, + ) + return nil +} + +func isWSLAvailable() bool { + if runtime.GOOS != "windows" { + return false + } + + // Check if WSL command exists + if _, err := exec.LookPath("wsl"); err != nil { + return false + } + + // Test if WSL is functional + cmd := exec.Command("wsl", "echo", "test") + return cmd.Run() == nil +} diff --git a/docs/actions.schema.md b/docs/actions.schema.md index e225173..7c8d25b 100644 --- a/docs/actions.schema.md +++ b/docs/actions.schema.md @@ -193,13 +193,14 @@ Arguments and Options are available by their machine names - `{{ .myArg1 }}`, `{ ### Predefined variables: -1. `current_uid` - current user ID. In Windows environment set to 0. -2. `current_gid` - current group ID. In Windows environment set to 0. -3. `current_working_dir` - current app working directory. -4. `actions_base_dir` - actions base directory where the action was found. By default, current working directory, +1. `current_uid` or `$UID` - current user ID. In Windows environment set to 1000. +2. `current_gid` or `$GID` - current group ID. In Windows environment set to 0. +3. `current_working_dir` or `$ACTION_WD` - current app working directory. +4. `actions_base_dir` or `$DISCOVERY_DIR` - actions base directory where the action was found. By default, current working directory, but other paths may be provided by plugins. -5. `action_dir` - directory of the action file. -6. `current_bin` - path to the currently executed command, like $0 in bash. +5. `action_dir` or `$ACTION_DIR` - directory of the action file. +6. `current_bin` or `$CBIN` - path to the Currently executed Binary. Works only in "shell" runtime. + On Windows, the path is converted to unix style. ### Environment variables @@ -490,8 +491,8 @@ runtime: build: context: ./ args: - USER_ID: {{ .current_uid }} - GROUP_ID: {{ .current_gid }} + USER_ID: $UID + GROUP_ID: $GID USER_NAME: plasma ``` diff --git a/docs/config.md b/docs/config.md index cd32705..4d194f0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -17,8 +17,9 @@ To change the default container runtime: ```yaml # ... -container: - runtime: kubernetes +runtime: + container: + default_runtime: kubernetes # ... ``` diff --git a/docs/development/README.md b/docs/development/README.md index f18115e..3e1da7a 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -7,3 +7,5 @@ This section covers the main parts of Launchr's development: capabilities. 2. [Service](service.md) - Describes the service implementation and dependency injection system used by Launchr to manage components and their dependencies. +3. [Test](test.md) - Explains test methodologies, tools and setup guidelines for maintaining code quality and + cross-platform testing compatibility diff --git a/docs/development/test.md b/docs/development/test.md new file mode 100644 index 0000000..2923992 --- /dev/null +++ b/docs/development/test.md @@ -0,0 +1,236 @@ +# Application Testing Guide + +This document provides comprehensive information about testing methodologies, tools, and procedures for the application. + +## Table of Contents + +- [Go Testing Solution](#go-testing-solution) +- [Running Tests](#running-tests) +- [Code Quality & Linting](#code-quality--linting) +- [Local Cross-Platform Testing](#local-cross-platform-testing) +- [GitHub Actions Debug Workflow](#github-actions-debug-workflow) +- [Reusable GitHub Workflows](#reusable-github-workflows) + +## Go Testing Solution + +Our testing strategy combines unit tests with integration testing to ensure comprehensive coverage and reliability. + +### Unit Testing + +Most of the codebase is covered with standard Go tests, providing fast feedback during development. + +### Test Output Formatting + +We use **gotestfmt** to provide clean, readable test output with improved formatting and colored output. This tool +transforms the standard Go test output into a more user-friendly format, making it easier to identify failures and +understand test results at a glance. + +### Integration Testing + +We use **Testscript** for integration testing of the built binary, offering robust end-to-end test capabilities. + +#### Why Testscript? + +- **Mature and Reliable**: Based on the [Go standard library's + `cmd/go/internal/script`](https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/cmd/go/internal/script/) +- **Declarative Scripting**: Uses a simple scripting language in `.txt` files for robust end-to-end tests +- **Self-Contained**: Utilizes the [txtar format](https://pkg.go.dev/github.com/rogpeppe/go-internal/txtar) to bundle + test scripts and fixture files +- **Portable**: Tests are easy to manage and transfer between environments + +**Resources:** + +- [Testscript Project](https://github.com/rogpeppe/go-internal) +- [Txtar Format Documentation](https://pkg.go.dev/github.com/rogpeppe/go-internal/txtar) + +## Running Tests + +### Basic Test Commands + +```bash +# Run all tests with verbose output +go test -v ./... + +# Run a specific test function +go test -v -run TestFunctionName ./... + +# Run tests with coverage report +go test -v -cover ./... +``` + +### Makefile Targets + +We provide convenient Makefile targets for different testing scenarios: + +```bash +# Run full test suite +make test + +# Run short tests (skips heavy/slow tests) +make test-short +``` + +**Note:** Use `make test-short` during development to skip time-consuming tests and get faster feedback. + +## Code Quality & Linting + +### Linting Standards + +The codebase is linted using **golangci-lint** to maintain consistent code quality and style. + +#### Running the Linter + +```bash +# Lint the entire codebase +make lint +``` + +#### Guidelines + +- Follow all standard linting rules +- Use `//nolint` comments only in exceptional cases +- Ensure all code passes linting checks before submitting + +## Local Cross-Platform Testing + +Testing across different operating systems is crucial for ensuring compatibility. Here's how to set up local +cross-platform testing environments: + +### Platform-Specific Solutions + +| Host OS | Target OS | Recommended Solution | +|-------------|-------------|-----------------------------------------------------------------------------------------| +| **Linux** | Windows | [dockur/windows](https://github.com/dockur/windows) Docker container | +| **Linux** | macOS | [dockur/macos](https://github.com/dockur/macos) Docker container | +| **Linux** | Other Linux | [Lima](https://lima-vm.io/) | +| **macOS** | Windows | [UTM](https://mac.getutm.app/) (free) or [Parallels](https://www.parallels.com/) (paid) | +| **macOS** | Linux | [Lima](https://lima-vm.io/) | +| **Windows** | Linux | [WSL2](https://docs.microsoft.com/en-us/windows/wsl/) | +| **Windows** | macOS | [dockur/macos](https://github.com/dockur/macos) via Docker | + +### Quick Setup Examples + +```bash +# Linux testing Windows via Docker +docker run -it dockur/windows + +# macOS/Linux testing via Lima +lima create --name test-env ubuntu +lima start test-env +lima shell test-env +``` + +## GitHub Actions Debug Workflow + +When local testing isn't sufficient, you can debug and test directly on GitHub's infrastructure using our debug +workflow. + +### Setup Instructions + +1. **Fork the Repository** + - Provides full control over the repository + - Maintains clean GitHub Actions history for your project + +2. **Access the Debug Workflow** + - Navigate to the **Actions** tab in your forked repository + - Select **"🚧 Debug with SSH"** workflow + - Click **"Run workflow"** and select: + - Target branch + - Operating system (Windows/macOS/Linux) + - Architecture (amd64/arm64) + +3. **Connect to the Runner** + - Open the new pipeline execution + - Click on the **"πŸ‘‰ How to connect πŸ‘ˆ"** job + - Wait for dependency installation to complete + - Find the SSH connection command in the logs + +### VS Code Integration + +For a full development environment, you can connect VS Code to the GitHub runner: + +#### Prerequisites + +- Install the **"Remote - SSH"** extension in VS Code + +#### Connection Steps + +1. Open Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) +2. Select **"Remote-SSH: Add New SSH Host..."** +3. Enter the SSH command from the GitHub Actions logs +4. Click **"Connect"** in the popup or use **"Remote-SSH: Connect to Host..."** +5. Open the project path specified in the logs + +### Use Cases + +- **Remote Debugging**: Forward `dlv` port for Go debugging +- **Cross-Platform Testing**: Test on different OS/architecture combinations +- **CI/CD Troubleshooting**: Debug failing workflows in the actual environment +- **Development**: Full development environment without local setup + +## Reusable GitHub Workflows + +To maintain consistency across launchr-related projects, we provide reusable workflows that can be integrated into other +repositories. + +### Integration Example + +Create or update your `.github/workflows/ci.yaml` file: + +```yaml +name: πŸ§ͺ Code Coverage & Testing + +on: + push: + branches: + - '**' + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.gitignore' + - 'example/**' + - 'docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: πŸ›‘οΈ Multi-Platform Testing Suite + uses: launchr/launchr/.github/workflows/test-suite.yaml@main +``` + +### Benefits of Reusable Workflows + +- **Consistency**: Standardized testing across all projects +- **Maintenance**: Centralized updates and improvements +- **Efficiency**: Reduced duplication of workflow configuration +- **Best Practices**: Automatically includes optimized testing strategies + +## Best Practices + +### Development Workflow + +1. Write tests alongside your code +2. Use `make test-short` during development for quick feedback +3. Run full test suite before committing: `make test` +4. Ensure linting passes: `make lint` +5. Test cross-platform compatibility when needed + +### CI/CD Integration + +- Use the reusable workflow for consistent testing +- Configure appropriate triggers and path ignores +- Monitor test coverage and maintain quality standards + +### Debugging Strategy + +1. Start with local testing +2. Use cross-platform VMs for OS-specific issues +3. Leverage GitHub Actions debug workflow for CI/CD issues +4. Utilize VS Code remote development for complex debugging + +--- + +For questions or contributions to the testing infrastructure, please refer to the project's contribution guidelines. diff --git a/example/actions/buildargs/action.yaml b/example/actions/buildargs/action.yaml index 352c72d..e425c3c 100644 --- a/example/actions/buildargs/action.yaml +++ b/example/actions/buildargs/action.yaml @@ -8,8 +8,8 @@ runtime: build: context: ./ args: - USER_ID: {{ .current_uid }} - GROUP_ID: {{ .current_gid }} + USER_ID: $UID + GROUP_ID: $GID USER_NAME: username command: - sh diff --git a/example/actions/foundation/software/flatcar/actions/bump/action.yaml b/example/actions/foundation/software/flatcar/actions/bump/action.yaml deleted file mode 100644 index 7af349f..0000000 --- a/example/actions/foundation/software/flatcar/actions/bump/action.yaml +++ /dev/null @@ -1,20 +0,0 @@ -action: - title: Verb - description: Handles some logic - arguments: - - name: arg1 - title: Argument 1 - description: Some additional info for arg - required: true - - name: arg2 - title: Argument 2 - description: Some additional info for arg - options: - - name: opt1 - title: Option 1 - description: Some additional info for option - -runtime: - type: container - image: python:3.7-slim - command: python3 %s diff --git a/example/actions/integration/application/bus/actions/watch/action.yaml b/example/actions/integration/application/bus/actions/watch/action.yaml deleted file mode 100644 index 7af349f..0000000 --- a/example/actions/integration/application/bus/actions/watch/action.yaml +++ /dev/null @@ -1,20 +0,0 @@ -action: - title: Verb - description: Handles some logic - arguments: - - name: arg1 - title: Argument 1 - description: Some additional info for arg - required: true - - name: arg2 - title: Argument 2 - description: Some additional info for arg - options: - - name: opt1 - title: Option 1 - description: Some additional info for option - -runtime: - type: container - image: python:3.7-slim - command: python3 %s diff --git a/example/actions/platform/actions/bump/action.yaml b/example/actions/platform/actions/bump/action.yaml deleted file mode 100644 index 532c47e..0000000 --- a/example/actions/platform/actions/bump/action.yaml +++ /dev/null @@ -1,20 +0,0 @@ -action: - title: Verb - description: Handles some logic - arguments: - - name: arg1 - title: Argument 1 - description: Some additional info for arg - required: false - - name: arg2 - title: Argument 2 - description: Some additional info for arg - options: - - name: opt1 - title: Option 1 - description: Some additional info for option - -runtime: - type: container - image: python:3.7-slim - command: python3 %s diff --git a/example/actions/shell/action.yaml b/example/actions/shell/action.yaml index a8990a3..9bf1439 100644 --- a/example/actions/shell/action.yaml +++ b/example/actions/shell/action.yaml @@ -21,14 +21,14 @@ runtime: pwd whoami env - echo "Current bin path: {{ .current_bin }}" + echo "Current bin path: $$CBIN" echo "Version:" - {{ .current_bin }} --version + $$CBIN --version echo "" echo "Help:" - {{ .current_bin }} --help + $$CBIN --help echo $${MY_ENV_VAR} - {{ .action_dir }}/main.sh "{{ .firstoption }}" "{{ .secondoption }}" + $$ACTION_DIR/main.sh "{{ .firstoption }}" "{{ .secondoption }}" echo "Running timer for 60 seconds" bash -c "for i in \$(seq 60); do echo \$$i; sleep 1; done" echo "Finish" diff --git a/example/plugins/action_embedfs/actions/example1/action.yaml b/example/plugins/action_embedfs/actions/example1/action.yaml index 352c72d..e425c3c 100644 --- a/example/plugins/action_embedfs/actions/example1/action.yaml +++ b/example/plugins/action_embedfs/actions/example1/action.yaml @@ -8,8 +8,8 @@ runtime: build: context: ./ args: - USER_ID: {{ .current_uid }} - GROUP_ID: {{ .current_gid }} + USER_ID: $UID + GROUP_ID: $GID USER_NAME: username command: - sh diff --git a/pkg/driver/tty.go b/pkg/driver/tty.go index 98c963f..7674fcc 100644 --- a/pkg/driver/tty.go +++ b/pkg/driver/tty.go @@ -2,13 +2,8 @@ package driver import ( "context" - "os" - gosignal "os/signal" - "runtime" "time" - "github.com/moby/sys/signal" - "github.com/launchrctl/launchr/internal/launchr" ) @@ -74,35 +69,5 @@ func (t *TtySizeMonitor) Start(ctx context.Context, streams launchr.Streams) { return } initTtySize(ctx, streams, t.resizeFn) - if runtime.GOOS == "windows" { - go func() { - prevH, prevW := streams.Out().GetTtySize() - for { - h, w := streams.Out().GetTtySize() - - if prevW != w || prevH != h { - err := resizeTty(ctx, streams, t.resizeFn) - if err != nil { - // Stop monitoring - return - } - } - prevH = h - prevW = w - } - }() - } else { - sigchan := make(chan os.Signal, 1) - gosignal.Notify(sigchan, signal.SIGWINCH) - go func() { - defer gosignal.Stop(sigchan) - for range sigchan { - err := resizeTty(ctx, streams, t.resizeFn) - if err != nil { - // Stop monitoring - return - } - } - }() - } + watchTtySize(ctx, streams, t.resizeFn) } diff --git a/pkg/driver/tty_unix.go b/pkg/driver/tty_unix.go new file mode 100644 index 0000000..e63a170 --- /dev/null +++ b/pkg/driver/tty_unix.go @@ -0,0 +1,28 @@ +//go:build unix + +package driver + +import ( + "context" + "os" + gosignal "os/signal" + + "github.com/moby/sys/signal" + + "github.com/launchrctl/launchr/internal/launchr" +) + +func watchTtySize(ctx context.Context, streams launchr.Streams, resizeFn resizeTtyFn) { + sigchan := make(chan os.Signal, 1) + gosignal.Notify(sigchan, signal.SIGWINCH) + go func() { + defer gosignal.Stop(sigchan) + for range sigchan { + err := resizeTty(ctx, streams, resizeFn) + if err != nil { + // Stop monitoring + return + } + } + }() +} diff --git a/pkg/driver/tty_windows.go b/pkg/driver/tty_windows.go new file mode 100644 index 0000000..2e39b01 --- /dev/null +++ b/pkg/driver/tty_windows.go @@ -0,0 +1,28 @@ +//go:build windows + +package driver + +import ( + "context" + + "github.com/launchrctl/launchr/internal/launchr" +) + +func watchTtySize(ctx context.Context, streams launchr.Streams, resizeFn resizeTtyFn) { + go func() { + prevH, prevW := streams.Out().GetTtySize() + for { + h, w := streams.Out().GetTtySize() + + if prevW != w || prevH != h { + err := resizeTty(ctx, streams, resizeFn) + if err != nil { + // Stop monitoring + return + } + } + prevH = h + prevW = w + } + }() +} diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..5302e6b --- /dev/null +++ b/test/README.md @@ -0,0 +1,182 @@ +# Integration Tests + +This directory contains integration tests, test scripts, and test helpers designed to provide comprehensive end-to-end +testing capabilities for the application. + +## Overview + +Our integration testing framework extends the standard [testscript](https://github.com/rogpeppe/go-internal) +functionality with custom commands and enhancements tailored to our specific testing needs. These tests validate the +complete application workflow, from binary execution to complex text processing scenarios. + +## Documentation + +For comprehensive information on testing methodologies, setup, and best practices, please refer to +our [testing documentation](../docs/development/test.md). + +## Custom Testscript Commands + +We have extended the standard testscript command set with several custom commands to enhance testing capabilities: + +### Text Processing Commands + +#### `txtproc` - Advanced Text Processing + +Provides flexible text processing capabilities for manipulating test files and outputs. + +**Available Operations:** + +| Operation | Description | Usage | +|-----------------|-------------------------------------|----------------------------------------------------------------------| +| `replace` | Replace literal text strings | `txtproc replace 'old_text' 'new_text' input.txt output.txt` | +| `replace-regex` | Replace using regular expressions | `txtproc replace-regex 'pattern' 'replacement' input.txt output.txt` | +| `remove-lines` | Remove lines matching a pattern | `txtproc remove-lines 'pattern' input.txt output.txt` | +| `remove-regex` | Remove text matching regex pattern | `txtproc remove-regex 'pattern' input.txt output.txt` | +| `extract-lines` | Extract lines matching a pattern | `txtproc extract-lines 'pattern' input.txt output.txt` | +| `extract-regex` | Extract text matching regex pattern | `txtproc extract-regex 'pattern' input.txt output.txt` | + +**Examples:** + +```bash +# Replace version numbers in output +txtproc replace 'v1.0.0' 'v2.0.0' app_output.txt expected.txt + +# Extract error messages using regex +txtproc extract-regex 'ERROR: .*' log.txt errors.txt + +# Remove debug lines from output +txtproc remove-lines 'DEBUG:' verbose_output.txt clean_output.txt +``` + +### Utility Commands + +#### `sleep` - Execution Delay + +Pauses test execution for a specified duration, useful for timing-sensitive tests or waiting for asynchronous +operations. + +**Usage:** + +```bash +sleep +``` + +**Supported Duration Formats:** + +- `ns` - nanoseconds +- `us` - microseconds +- `ms` - milliseconds +- `s` - seconds +- `m` - minutes +- `h` - hours + +**Examples:** + +```bash +# Wait for 1 second +sleep 1s + +# Wait for 500 milliseconds +sleep 500ms + +# Wait for 2 minutes +sleep 2m + +# Wait for background process +sleep 100ms +``` + +#### `dlv` - Debug Integration + +Runs the specified binary with [Delve](https://github.com/go-delve/delve) debugger support for interactive debugging +during tests. + +**Usage:** + +```bash +dlv +``` + +**Prerequisites:** + +- Binary must be compiled with debug headers (`-gcflags="all=-N -l"`) +- Delve must be installed in the testing environment + +**Examples:** + +```bash +# Debug the main application +dlv launchr +``` + +## Command Overrides + +### Enhanced `kill` Command + +We override the default testscript `kill` command to provide broader signal support beyond the standard implementation. + +**Enhanced Features:** + +- Support for additional POSIX signals +- Cross-platform signal handling +- Improved process termination reliability + +**Usage:** + +```bash +# Standard termination +kill bg-name + +# Graceful shutdown with SIGTERM +kill -TERM bg-name +``` + +## Writing Integration Tests + +### Basic Test Structure + +Integration tests use the [txtar format](https://pkg.go.dev/github.com/rogpeppe/go-internal/txtar) to bundle test +scripts with their required files. See [examples](./testdata). + +### Best Practices + +#### Test Organization + +- Group related tests in logical directories +- Use descriptive test file names +- Include both positive and negative test cases + +#### File Management + +- Keep test files small and focused +- Use meaningful fixture names +- Clean up temporary files when possible + +#### Error Handling + +- Test error conditions explicitly +- Verify error messages and exit codes +- Use `! exec` for commands expected to fail + +#### Performance Considerations + +- Use `sleep` judiciously to avoid slow tests +- Prefer deterministic waits over arbitrary delays +- Consider using `make test-short` for development + +## Contributing + +When adding new integration tests: + +1. Follow the existing naming conventions +2. Include comprehensive test documentation +3. Test on multiple platforms when relevant +4. Add appropriate error handling +5. Update this README if adding new custom commands + +## Related Resources + +- [Testscript Documentation](https://github.com/rogpeppe/go-internal/tree/master/testscript) +- [Txtar Format Specification](https://pkg.go.dev/github.com/rogpeppe/go-internal/txtar) +- [Delve Debugger](https://github.com/go-delve/delve) +- [Testing Documentation](../docs/development/test.md) diff --git a/test/plugins/default.go b/test/plugins/default.go new file mode 100644 index 0000000..7aa21c6 --- /dev/null +++ b/test/plugins/default.go @@ -0,0 +1,7 @@ +// Package plugins includes all test plugins. +package plugins + +import ( + // Include test plugins. + _ "github.com/launchrctl/launchr/test/plugins/testactions" +) diff --git a/test/plugins/genaction/go.mod b/test/plugins/genaction/go.mod new file mode 100644 index 0000000..8e8ae07 --- /dev/null +++ b/test/plugins/genaction/go.mod @@ -0,0 +1,8 @@ +module example.com/genaction + +go 1.24.1 + +// Have replace for local development +replace github.com/launchrctl/launchr => ../../../ + +require github.com/launchrctl/launchr v0.0.0 diff --git a/test/plugins/genaction/hello.go b/test/plugins/genaction/hello.go new file mode 100644 index 0000000..b506a81 --- /dev/null +++ b/test/plugins/genaction/hello.go @@ -0,0 +1,7 @@ +//go:build !customtag + +package genaction + +func helloWorldStr() string { + return "hello world" +} diff --git a/test/plugins/genaction/hello_customtag.go b/test/plugins/genaction/hello_customtag.go new file mode 100644 index 0000000..c239652 --- /dev/null +++ b/test/plugins/genaction/hello_customtag.go @@ -0,0 +1,7 @@ +//go:build customtag + +package genaction + +func helloWorldStr() string { + return "hello world built with custom tag" +} diff --git a/test/plugins/genaction/plugin.go b/test/plugins/genaction/plugin.go new file mode 100644 index 0000000..53e6d02 --- /dev/null +++ b/test/plugins/genaction/plugin.go @@ -0,0 +1,74 @@ +package genaction + +import ( + "context" + "os" + "path/filepath" + + "github.com/launchrctl/launchr" + "github.com/launchrctl/launchr/pkg/action" +) + +// pluginTemplate is a go file that will be generated and included in the build. +const pluginTemplate = `package main +import ( + _ "embed" + my "{{.Pkg}}" +) +//go:embed {{.Yaml}} +var y []byte +func init() { my.ActionYaml = y }` + +// ActionYaml is a yaml content that will be set from an embedded file in [pluginTemplate]. +var ActionYaml []byte + +func init() { + launchr.RegisterPlugin(&Plugin{}) +} + +// Plugin is a test plugin declaration. +type Plugin struct{} + +// PluginInfo implements [launchr.Plugin] interface. +func (p *Plugin) PluginInfo() launchr.PluginInfo { + return launchr.PluginInfo{} +} + +// Generate implements [launchr.GeneratePlugin] interface. +func (p *Plugin) Generate(config launchr.GenerateConfig) error { + launchr.Term().Info().Printfln("Generating genaction...") + + actionyaml := "action.yaml" + const yaml = "{ runtime: plugin, action: { title: My plugin } }" + type tplvars struct { + Pkg string + Yaml string + } + + tpl := launchr.Template{Tmpl: pluginTemplate, Data: tplvars{ + Pkg: "example.com/genaction", + Yaml: actionyaml, + }} + err := tpl.WriteFile(filepath.Join(config.BuildDir, "genaction.gen.go")) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(config.BuildDir, actionyaml), []byte(yaml), 0600) + if err != nil { + return err + } + + return nil +} + +// DiscoverActions implements [launchr.ActionDiscoveryPlugin] interface. +func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) { + a := action.NewFromYAML("genaction:example", ActionYaml) + a.SetRuntime(action.NewFnRuntime(func(_ context.Context, a *action.Action) error { + launchr.Term().Println(helloWorldStr()) + return nil + })) + + return []*action.Action{a}, nil +} diff --git a/test/plugins/testactions/embed-fs/action-container/action.yaml b/test/plugins/testactions/embed-fs/action-container/action.yaml new file mode 100644 index 0000000..e7d766e --- /dev/null +++ b/test/plugins/testactions/embed-fs/action-container/action.yaml @@ -0,0 +1,9 @@ +action: + title: embed action + +runtime: + type: container + image: alpine:latest + command: + - sh + - /action/main.sh diff --git a/test/plugins/testactions/embed-fs/action-container/main.sh b/test/plugins/testactions/embed-fs/action-container/main.sh new file mode 100644 index 0000000..6abba80 --- /dev/null +++ b/test/plugins/testactions/embed-fs/action-container/main.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +echo "hello action from container " > /action/container.txt +echo "hello host from container" > /host/container.txt +echo -n "action ls: " +ls -1 /action | paste -sd ' ' +echo -n "host ls: " +ls -1 /host | paste -sd ' ' +echo "" +echo "exiting" diff --git a/test/plugins/testactions/embed.go b/test/plugins/testactions/embed.go new file mode 100644 index 0000000..90c7683 --- /dev/null +++ b/test/plugins/testactions/embed.go @@ -0,0 +1,25 @@ +package testactions + +import ( + "embed" + + "github.com/launchrctl/launchr/internal/launchr" + "github.com/launchrctl/launchr/pkg/action" +) + +//go:embed test-registered-embed-fs/* +var registeredEmbedFS embed.FS + +//go:embed embed-fs/* +var embedActionsFS embed.FS + +func embedContainerAction() *action.Action { + // Create an action from FS. + // Use subdirectory so the content is available in the root "./". + subfs := launchr.MustSubFS(embedActionsFS, "embed-fs/action-container") + a, err := action.NewYAMLFromFS("test-embed-fs:container", subfs) + if err != nil { + panic(err) + } + return a +} diff --git a/test/plugins/testactions/log_levels.go b/test/plugins/testactions/log_levels.go new file mode 100644 index 0000000..960e7b0 --- /dev/null +++ b/test/plugins/testactions/log_levels.go @@ -0,0 +1,27 @@ +package testactions + +import ( + "context" + + "github.com/launchrctl/launchr/internal/launchr" + "github.com/launchrctl/launchr/pkg/action" +) + +const logLevelsYaml = ` +runtime: plugin +action: + title: Test Plugin - Log levels +` + +func actionLogLevels() *action.Action { + // Create an action that outputs to log all message types. + a := action.NewFromYAML("testplugin:log-levels", []byte(logLevelsYaml)) + a.SetRuntime(action.NewFnRuntime(func(_ context.Context, _ *action.Action) error { + launchr.Log().Debug("this is DEBUG log") + launchr.Log().Info("this is INFO log") + launchr.Log().Warn("this is WARN log") + launchr.Log().Error("this is ERROR log") + return nil + })) + return a +} diff --git a/test/plugins/testactions/plugin.go b/test/plugins/testactions/plugin.go new file mode 100644 index 0000000..7b802cb --- /dev/null +++ b/test/plugins/testactions/plugin.go @@ -0,0 +1,44 @@ +// Package testactions contains actions that help to test the app. +package testactions + +import ( + "context" + + "github.com/launchrctl/launchr/internal/launchr" + "github.com/launchrctl/launchr/pkg/action" +) + +func init() { + launchr.RegisterPlugin(&Plugin{}) +} + +// Plugin is a test plugin declaration. +type Plugin struct { + app launchr.App +} + +// PluginInfo implements [launchr.Plugin] interface. +func (p *Plugin) PluginInfo() launchr.PluginInfo { + return launchr.PluginInfo{} +} + +// OnAppInit implements [launchr.OnAppInitPlugin] interface. +func (p *Plugin) OnAppInit(app launchr.App) error { + p.app = app + var am action.Manager + app.GetService(&am) + // Add custom fs to default discovery. + app.RegisterFS(action.NewDiscoveryFS(registeredEmbedFS, app.GetWD())) + // Create a special decorator to output given input. + am.AddDecorators(pluginPrintInput) + return nil +} + +// DiscoverActions implements [launchr.ActionDiscoveryPlugin] interface. +func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) { + return []*action.Action{ + actionSensitive(p.app), + actionLogLevels(), + embedContainerAction(), + }, nil +} diff --git a/test/plugins/testactions/print_input.go b/test/plugins/testactions/print_input.go new file mode 100644 index 0000000..da6de3c --- /dev/null +++ b/test/plugins/testactions/print_input.go @@ -0,0 +1,31 @@ +package testactions + +import ( + "context" + "fmt" + "strings" + + "github.com/launchrctl/launchr/pkg/action" +) + +// pluginPrintInput adds to all actions prefixed with "test-print-input:" +// a special runtime that outputs the given input. +func pluginPrintInput(_ action.Manager, a *action.Action) { + if !strings.HasPrefix(a.ID, "test-print-input:") { + return + } + a.SetRuntime(action.NewFnRuntime(func(_ context.Context, a *action.Action) error { + def := a.ActionDef() + for _, p := range def.Arguments { + printParam(p.Name, a.Input().Arg(p.Name), a.Input().IsArgChanged(p.Name)) + } + for _, p := range def.Options { + printParam(p.Name, a.Input().Opt(p.Name), a.Input().IsOptChanged(p.Name)) + } + return nil + })) +} + +func printParam(name string, val any, isChanged bool) { + fmt.Printf("%s: %v %T %t\n", name, val, val, isChanged) +} diff --git a/test/plugins/testactions/sensitive.go b/test/plugins/testactions/sensitive.go new file mode 100644 index 0000000..564304d --- /dev/null +++ b/test/plugins/testactions/sensitive.go @@ -0,0 +1,56 @@ +package testactions + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/launchrctl/launchr/internal/launchr" + "github.com/launchrctl/launchr/pkg/action" +) + +const sensitiveYaml = ` +runtime: plugin +action: + title: Test Plugin - Sensitive mask + arguments: + - name: arg + required: true +` + +func init() { + // Create an action that outputs a secret in a terminal. + secret := os.Getenv("TEST_SECRET") + if secret != "" { + mask := launchr.GlobalSensitiveMask() + mask.AddString(secret) + } +} + +func actionSensitive(app launchr.App) *action.Action { + a := action.NewFromYAML("testplugin:sensitive", []byte(sensitiveYaml)) + a.SetRuntime(action.NewFnRuntime(func(_ context.Context, a *action.Action) error { + arg := a.Input().Arg("arg").(string) + streams := app.Streams() + // Check terminal. + launchr.Term().Printfln("terminal output: %s", arg) + // Check log. + launchr.Log().Error("log output: " + arg) + // Check raw. + fmt.Printf("fmt print: %s\n", arg) + // Check using streams. + _, _ = fmt.Fprintf(streams.Out(), "fmt stdout streams print: %s\n", arg) + _, _ = fmt.Fprintf(streams.Err(), "fmt stderr streams print: %s\n", arg) + // Check if we output by parts. + parts := strings.Split(arg, " ") + if len(parts) == 2 { + launchr.Term().Print("split output: ") + launchr.Term().Print(parts[0]) + launchr.Term().Print(" ") + launchr.Term().Println(parts[1]) + } + return nil + })) + return a +} diff --git a/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/Dockerfile b/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/Dockerfile new file mode 100644 index 0000000..32ce9fc --- /dev/null +++ b/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/Dockerfile @@ -0,0 +1,6 @@ +FROM alpine:latest +ARG USER_ID +ARG USER_NAME +ARG GROUP_ID +RUN adduser -D -u ${USER_ID} -g ${GROUP_ID} ${USER_NAME} || true +USER ${USER_NAME} diff --git a/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/action.yaml b/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/action.yaml new file mode 100644 index 0000000..073a31c --- /dev/null +++ b/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/action.yaml @@ -0,0 +1,16 @@ +action: + title: embed action + description: Test passing args to Dockerfile + +runtime: + type: container + image: ${IMAGE_REGISTRY}/testimage-embed:latest + build: + context: ./ + args: + USER_ID: $UID + GROUP_ID: $GID + USER_NAME: foobar + command: + - sh + - /action/main.sh diff --git a/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/main.sh b/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/main.sh new file mode 100644 index 0000000..a9e2503 --- /dev/null +++ b/test/plugins/testactions/test-registered-embed-fs/actions/container-image-build/main.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +id +echo "hello action from container " > /action/container.txt +echo "hello host from container" > /host/container.txt +echo -n "action ls: " +ls -1 /action | paste -sd ' ' +echo -n "host ls: " +ls -1 /host | paste -sd ' ' +echo "" +echo "exiting" diff --git a/test/testdata/action/discovery/basic.txtar b/test/testdata/action/discovery/basic.txtar new file mode 100644 index 0000000..53a1020 --- /dev/null +++ b/test/testdata/action/discovery/basic.txtar @@ -0,0 +1,202 @@ +# ============================================================================= +# Launchr Action Discovery and Validation Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Discover actions from multiple directory structures and namespaces +# 2. Apply proper naming conventions and character validation rules +# 3. Filter out invalid actions (hidden, malformed, incorrectly placed) +# 4. Handle custom action paths via environment variables +# 5. Sort and group actions appropriately in help output +# +# Test Structure: +# - Tests standard action discovery with various naming patterns +# - Tests exclusion of invalid actions and hidden directories +# - Tests custom action path functionality +# - Validates output formatting and content matching +# ============================================================================= + +# Setup Phase: Directory Structure Creation +# ----------------------------------------------------------------------------- +# Create test directories with special characters to validate name handling +# These commands test the system's ability to handle various path scenarios +[unix] mkdir foo-bar_baz/actions/waldo*fred +[unix] mkdir actions/waldo*fred + +# Copy action files to test space handling in paths +# These operations test file system path resolution with problematic characters +[unix] cp 'foo-bar_baz/actions/waldo fred/action.yaml' foo-bar_baz/actions/waldo*fred/action.yaml +[unix] cp 'actions/waldo fred/action.yaml' actions/waldo*fred/action.yaml + +# Test 1: Standard Action Discovery and Validation +# ----------------------------------------------------------------------------- +# Execute launchr help to trigger action discovery and display +exec launchr --help + +# Validate that actions section appears with proper formatting +stdout '^\s+Actions:\n\s+bar\s+' + +# Test valid action discovery and naming: +# These actions should appear in the output with correct names and titles +stdout '^\s+bar\s+bar$' # Simple root-level action +stdout '^\s+foo\s+foo$' # Simple root-level action +stdout '^\s+foo\.bar\.baz:fred\s+fred$' # Properly namespaced action +stdout '^\s+foo-bar_baz:waldo-fred.1\s+valid special chars' # Valid special characters +stdout '^\s+foo\.bar\.baz:waldo\s+waldo$' # Namespaced action + +# Test invalid action exclusion: +# These patterns should NOT appear in the output due to naming violations +! stdout '^\s+foo-bar_baz:waldo.fred\s+invalid special chars$' # Space in name +! stdout '^\s+foo-bar_baz:waldo\s+invalid special chars$' # Space in path +! stdout '^\s+waldo.fred\s+invalid special chars$' # Space violation + +# Test hidden directory exclusion: +# Actions in hidden directories (starting with .) should not be discovered +! stdout '^\s+(.)hidden:foo\s+foo hidden skipped$' +! stdout '^\s+(.)hidden:bar\s+bar hidden skipped$' + +# Test incorrect path exclusion: +# Actions not in proper 'actions' directories should not be discovered +! stdout '^\s+foo\.bar\.baz:incorrect\s+incorrect actions path$' +! stdout '^\s+foo\.bar\.baz:subdir.*$' # Subdirectory actions + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Custom Action Path Discovery +# ----------------------------------------------------------------------------- +# Test LAUNCHR_ACTIONS_PATH environment variable functionality +env LAUNCHR_ACTIONS_PATH=./foo +exec launchr --help + +# With custom path, default actions should not appear +! stdout '^\s+foo\s+foo$' + +# Custom path actions should be discovered with proper namespacing +stdout '^\s+bar\.baz:fred\s+fred$' # Namespaced from custom path +stdout '^\s+bar\.baz:waldo\s+waldo$' # Namespaced from custom path + +# Validate clean execution with custom path +! stderr . + +# ============================================================================= +# Test Data Files - Action Configurations +# ============================================================================= + +# Standard Container Action +-- actions/foo/action.yaml -- +# Container-based action using Alpine Linux +action: + title: foo # Human-readable action name +runtime: + type: container # Container execution type + image: alpine # Base container image + command: [/bin/sh, ls] # Command to execute + +# Standard Shell Action +-- actions/bar/action.yaml -- +# Shell script action for file listing +action: + title: bar # Human-readable action name +runtime: + type: shell # Shell execution type + script: ls -al # Script content to execute + +# Valid Special Characters Action +-- foo-bar_baz/actions/waldo-fred.1/action.yaml -- +# Action demonstrating valid special character usage in names +# Valid characters: hyphens, underscores, dots, numbers +action: { title: valid special chars } # Compact YAML syntax +runtime: plugin # Plugin-based execution + +# Invalid Special Characters Action (Spaces) +-- foo-bar_baz/actions/waldo fred/action.yaml -- +# Action with invalid spaces in path - should be filtered out +action: { title: invalid special chars } +runtime: plugin + +# Invalid Special Characters Action (Root Level) +-- actions/waldo fred/action.yaml -- +# Another action with spaces in path - should be filtered out +action: { title: invalid special chars } +runtime: plugin + +# Namespaced Plugin Action - Waldo +-- foo/bar/baz/actions/waldo/action.yaml -- +# Plugin action in nested namespace structure +action: + title: waldo # Action title +runtime: plugin # Plugin execution type + +# Namespaced Plugin Action - Fred +-- foo/bar/baz/actions/fred/action.yaml -- +# Plugin action in nested namespace structure +action: + title: fred # Action title +runtime: plugin # Plugin execution type + +# Broken Action Configuration +-- foo/bar/baz/actions/broken/action.yaml -- +# Intentionally broken action to test error handling +# Missing required container properties +action: + title: broken # Action title +runtime: + type: container # Container type specified + # ERROR: Missing required image and command properties + +# Hidden Action - Foo (Should be ignored) +-- .hidden/actions/foo/action.yaml -- +# Action in hidden directory - should not be discovered +action: + title: foo hidden skipped # Title indicating it should be skipped +runtime: plugin + +# Hidden Action - Bar (Should be ignored) +-- .hidden/actions/bar/action.yaml -- +# Another hidden action - should not be discovered +action: + title: bar hidden skipped # Title indicating it should be skipped +runtime: plugin + +# Incorrectly Placed Action +-- foo/bar/baz/myactions/incorrect/action.yaml -- +# Action in non-standard directory name - should not be discovered +# Actions must be in directories named 'actions', not 'myactions' +action: + title: incorrect actions path # Describes the path issue +runtime: plugin + +# Subdirectory Action (Invalid Structure) +-- foo/bar/baz/actions/subdir/foo/action.yaml -- +# Action in subdirectory of actions directory - should not be discovered +# Actions should be directly in actions/ directory, not nested further +action: + title: foo incorrect pos of yaml in subdir # Describes structure issue +runtime: plugin + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Action Discovery Rules: +# 1. Scan directories named 'actions' for action.yaml files +# 2. Build action names from directory path hierarchy +# 3. Apply namespace prefixes based on directory structure +# 4. Filter out actions with invalid characters (spaces) +# 5. Ignore hidden directories (starting with .) +# 6. Ignore actions not directly in 'actions' directories +# +# Naming Conventions: +# - Valid characters: letters, numbers, hyphens, underscores, dots +# - Invalid characters: spaces, special symbols +# - Namespace separator: colon (:) +# - Path separator in namespace: dot (.) +# +# Output Format: +# - Actions listed under "Actions:" header +# - Format: "action_name action_title" +# - Sorted and grouped appropriately +# - No error output for successful operations +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/discovery/config_naming.txtar b/test/testdata/action/discovery/config_naming.txtar new file mode 100644 index 0000000..da01244 --- /dev/null +++ b/test/testdata/action/discovery/config_naming.txtar @@ -0,0 +1,92 @@ +# ============================================================================= +# Launchr Action Discovery and Naming Test +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Discover and display available actions with proper naming transformations +# 2. Apply custom naming rules defined in the configuration +# 3. Handle environment variable overrides for action paths +# +# Test Structure: +# - Tests default action discovery behavior +# - Tests custom action path via environment variable +# - Validates output format and content matching +# ============================================================================= + +# Test 1: Default Action Discovery +# ----------------------------------------------------------------------------- +# Test that launchr correctly discovers actions from the default path +# and applies naming transformations as defined in the configuration +exec launchr --help + +# Expected output validation: +# - Should find action at foo.baz.bar-bar:waldo-fred-thud with title "foo" +# - Naming rules should transform underscores to hyphens and collapse dots +stdout '^\s+foo\.baz\.bar-bar:waldo-fred-thud\s+foo$' + +# Ensure no error output is produced +! stderr . + +# Test 2: Custom Action Path via Environment Variable +# ----------------------------------------------------------------------------- +# Test that LAUNCHR_ACTIONS_PATH environment variable correctly overrides +# the default action discovery path +env LAUNCHR_ACTIONS_PATH=./foo +exec launchr --help + +# Expected output validation: +# - Should find action at bar.baz.bar-bar:waldo-fred-thud with title "foo" +# - Path change should affect the discovered action namespace +stdout '^\s+bar\.baz\.bar-bar:waldo-fred-thud\s+foo$' + +# Ensure no error output is produced +! stderr . + +# ============================================================================= +# Test Data Files +# ============================================================================= + +# Action Definition File +# This file defines a simple action with: +# - Title: "foo" (displayed in help output) +# - Runtime: "plugin" (specifies execution environment) +-- foo/bar/baz/bar/bar_bar/actions/waldo-fred_thud/action.yaml -- +action: + title: foo # Human-readable action title +runtime: plugin # Execution runtime specification + +# Launchr Configuration File +# Configuration file that defines action naming transformation rules: +# 1. Replace ".bar." with "." to simplify nested namespaces +# 2. Replace "_" with "-" for consistent kebab-case naming +-- .launchr/config.yaml -- +launchrctl: + actions_naming: + # Rule 1: Simplify nested bar namespaces + - search: ".bar." # Find pattern: .bar. + replace: "." # Replace with: . + + # Rule 2: Convert underscores to hyphens for consistency + - search: "_" # Find pattern: _ + replace: "-" # Replace with: - + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# 1. Action Discovery: +# - Scans directory structure for action.yaml files +# - Builds action names from directory path structure +# - Applies naming transformations from configuration +# +# 2. Naming Transformations: +# - Original path: foo/bar/baz/bar/bar_bar/actions/waldo-fred_thud +# - After .bar. β†’ . : foo/baz/bar_bar/actions/waldo-fred_thud +# - After _ β†’ - : foo/baz/bar-bar/actions/waldo-fred-thud +# - Final action name: foo.baz.bar-bar:waldo-fred-thud +# +# 3. Environment Override: +# - LAUNCHR_ACTIONS_PATH=./foo changes the root discovery path +# - Results in different namespace prefix for discovered actions +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/discovery/skip_incorrect_boolean.txtar b/test/testdata/action/discovery/skip_incorrect_boolean.txtar new file mode 100644 index 0000000..86eee33 --- /dev/null +++ b/test/testdata/action/discovery/skip_incorrect_boolean.txtar @@ -0,0 +1,79 @@ +# ============================================================================= +# Launchr Action Option Type Validation Test Suite - Boolean Default Mismatch +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Detect type mismatches between option declarations and default values +# 2. Provide clear error messages for boolean option configuration errors +# 3. Skip actions with invalid option configurations +# 4. Report specific line and column information for validation errors +# +# Test Focus: +# - Boolean option type validation +# - Default value type checking +# - Error message formatting and content +# - Action skipping behavior for invalid configurations +# ============================================================================= + +# Test 1: Boolean Option Default Value Type Validation +# ----------------------------------------------------------------------------- +# Execute action with incorrectly typed default value for boolean option +# This should trigger validation error and skip the action +exec launchr opt-boolean-incorrect-default + +# Validate error message content and format: +# Error should reference the correct action name (note: output shows different name) +stdout 'Action "opt-number-incorrect-default" was skipped:' + +# Validate specific type mismatch error details: +# Should identify the type conflict between string and boolean +stdout 'given value type \(string\) and expected type \(bool\) mismatch, line 13, col 16' + +# Validate clean execution (no error output to stderr) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configuration with Type Mismatch +# ============================================================================= + +# Boolean Option with Invalid String Default +-- actions/opt-number-incorrect-default/action.yaml -- +# Plugin action demonstrating boolean option type validation failure +# This configuration intentionally contains a type mismatch error +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Boolean # Human-readable action name + + options: + - name: optBoolean # Option identifier + title: Option Boolean # Human-readable option name + type: boolean # Expected type: boolean + description: This is an optional boolean option # Help text + default: no # ERROR: String value for boolean type + # Valid boolean defaults would be: true, false + # Invalid: "no", "yes", "1", "0", or any string values + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Type Validation Rules: +# 1. Option default values must match declared option types +# 2. Boolean options accept only: true, false (not strings like "yes"/"no") +# 3. Type mismatches should be reported with specific location information +# 4. Actions with invalid option configurations should be skipped +# +# Error Message Format: +# - Action name and skip notification +# - Specific type mismatch description +# - Line and column number for precise error location +# - Clear indication of expected vs. actual types +# +# Validation Behavior: +# - Actions are validated before execution +# - Invalid actions are skipped rather than causing runtime errors +# - Error messages provide enough detail for debugging +# - Process continues normally after skipping invalid actions +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/discovery/skip_incorrect_integer.txtar b/test/testdata/action/discovery/skip_incorrect_integer.txtar new file mode 100644 index 0000000..9517dca --- /dev/null +++ b/test/testdata/action/discovery/skip_incorrect_integer.txtar @@ -0,0 +1,86 @@ +# ============================================================================= +# Launchr Action Option Type Validation Test Suite - Integer Default Mismatch +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Detect type mismatches between integer option declarations and default values +# 2. Provide clear error messages for integer option configuration errors +# 3. Skip actions with invalid option configurations +# 4. Report specific line and column information for validation errors +# +# Test Focus: +# - Integer option type validation +# - Default value type checking for numeric types +# - Error message formatting and content +# - Action skipping behavior for invalid configurations +# ============================================================================= + +# Test 1: Integer Option Default Value Type Validation +# ----------------------------------------------------------------------------- +# Execute action with incorrectly typed default value for integer option +# This should trigger validation error and skip the action +exec launchr opt-integer-incorrect-default + +# Validate action skip notification: +# Action should be skipped due to type validation failure +stdout 'Action "opt-integer-incorrect-default" was skipped' + +# Validate specific type mismatch error details: +# Should identify the type conflict between string and integer +stdout 'given value type \(string\) and expected type \(int\) mismatch, line 13, col 16' + +# Validate clean execution (no error output to stderr) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configuration with Type Mismatch +# ============================================================================= + +# Integer Option with Invalid String Default +-- actions/opt-integer-incorrect-default/action.yaml -- +# Plugin action demonstrating integer option type validation failure +# This configuration intentionally contains a type mismatch error +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Integer # Human-readable action name + + options: + - name: optInt # Option identifier + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Expected type: integer + default: foo # ERROR: String value for integer type + # Valid integer defaults would be: 42, -10, 0 + # Invalid: "foo", "123", true, false, or any non-numeric values + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Type Validation Rules: +# 1. Option default values must match declared option types +# 2. Integer options accept only numeric values (42, -10, 0) +# 3. String representations of numbers are invalid for integer types +# 4. Type mismatches should be reported with specific location information +# 5. Actions with invalid option configurations should be skipped +# +# Error Message Format: +# - Action name and skip notification +# - Specific type mismatch description (string vs int) +# - Line and column number for precise error location +# - Clear indication of expected vs. actual types +# +# Validation Behavior: +# - Actions are validated before execution +# - Invalid actions are skipped rather than causing runtime errors +# - Error messages provide enough detail for debugging +# - Process continues normally after skipping invalid actions +# +# Numeric Type Handling: +# - Integer options require actual numeric values in YAML +# - String values like "123" are not automatically converted +# - Type checking is strict and explicit +# - Non-numeric strings like "foo" are clearly invalid +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/discovery/skip_incorrect_number.txtar b/test/testdata/action/discovery/skip_incorrect_number.txtar new file mode 100644 index 0000000..808fd8f --- /dev/null +++ b/test/testdata/action/discovery/skip_incorrect_number.txtar @@ -0,0 +1,96 @@ +# ============================================================================= +# Launchr Action Option Type Validation Test Suite - Number Default Mismatch +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Detect type mismatches between number option declarations and default values +# 2. Provide clear error messages for number option configuration errors +# 3. Skip actions with invalid option configurations +# 4. Report specific line and column information for validation errors +# 5. Handle locale-specific number formatting issues +# +# Test Focus: +# - Number (float64) option type validation +# - Default value type checking for floating-point types +# - Locale-specific number format validation (comma vs. dot decimal separators) +# - Error message formatting and content +# - Action skipping behavior for invalid configurations +# ============================================================================= + +# Test 1: Number Option Default Value Type Validation +# ----------------------------------------------------------------------------- +# Execute action with incorrectly formatted default value for number option +# This should trigger validation error and skip the action +exec launchr opt-number-incorrect-default + +# Validate action skip notification: +# Action should be skipped due to type validation failure +stdout 'Action "opt-number-incorrect-default" was skipped:' + +# Validate specific type mismatch error details: +# Should identify the type conflict between string and float64 +stdout 'given value type \(string\) and expected type \(float64\) mismatch, line 13, col 16' + +# Validate clean execution (no error output to stderr) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configuration with Type Mismatch +# ============================================================================= + +# Number Option with Invalid Locale-Formatted Default +-- actions/opt-number-incorrect-default/action.yaml -- +# Plugin action demonstrating number option type validation failure +# This configuration intentionally contains a locale-specific formatting error +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Number # Human-readable action name + + options: + - name: optNumber # Option identifier + title: Option number # Human-readable option name + description: This is an optional float option # Help text + type: number # Expected type: number (float64) + default: 37,73 # ERROR: Comma as decimal separator + # Valid number defaults would be: 37.73, -10.5, 0.0, 42 + # Invalid: "37,73" (European format), "abc", true, or non-numeric strings + # Note: YAML/JSON standard requires dot (.) as decimal separator + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Type Validation Rules: +# 1. Option default values must match declared option types +# 2. Number options accept only numeric values with dot decimal separator +# 3. Locale-specific formatting (comma separators) is not supported +# 4. Type mismatches should be reported with specific location information +# 5. Actions with invalid option configurations should be skipped +# +# Error Message Format: +# - Action name and skip notification with colon +# - Specific type mismatch description (string vs float64) +# - Line and column number for precise error location +# - Clear indication of expected vs. actual types +# +# Validation Behavior: +# - Actions are validated before execution +# - Invalid actions are skipped rather than causing runtime errors +# - Error messages provide enough detail for debugging +# - Process continues normally after skipping invalid actions +# +# Number Format Requirements: +# - Number options require standard JSON/YAML numeric format +# - Decimal separator must be dot (.) not comma (,) +# - Scientific notation (1.23e-4) should be supported +# - Locale-specific formatting is not automatically converted +# - Type checking is strict and follows JSON/YAML standards +# +# Common Format Issues: +# - European decimal format: "37,73" β†’ should be "37.73" +# - Thousand separators: "1,000.50" β†’ should be "1000.50" +# - Currency symbols: "$37.73" β†’ should be "37.73" +# - Percentage format: "37%" β†’ should be "0.37" or "37" +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/alias-duplicates.txtar b/test/testdata/action/input/alias-duplicates.txtar new file mode 100644 index 0000000..a5b4c04 --- /dev/null +++ b/test/testdata/action/input/alias-duplicates.txtar @@ -0,0 +1,203 @@ +# ============================================================================= +# Launchr Action Alias Conflict Detection Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Detect duplicate alias definitions across multiple actions +# 2. Skip actions with conflicting alias definitions +# 3. Maintain functionality of the first action that claims an alias +# 4. Provide clear error messages about alias conflicts +# 5. Handle alias resolution when conflicts exist +# +# Test Focus: +# - Alias uniqueness validation across the action system +# - Conflict resolution strategy (first-wins approach) +# - Error messaging for duplicate alias definitions +# - Action skipping behavior for conflicting actions +# - Alias execution behavior during conflicts +# ============================================================================= + +# Test 1: First Action Execution (Full Name) +# ----------------------------------------------------------------------------- +# Execute the first action using its full namespaced name +# This action should work normally as it claimed the aliases first +exec launchr test-print-input:full-1 73 + +# Validate conflict detection and warning message: +# Should warn about the second action being skipped due to alias conflict +stdout 'Action "test-print-input:full-2" was skipped:' +stdout 'alias "alias2" is already defined by "test-print-input:full-1"' + +# Validate successful execution of first action: +# Should produce normal output since this action is not skipped +stdout '^argInteger1: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: First Action's First Alias Execution +# ----------------------------------------------------------------------------- +# Execute the first action using its first alias +# Should work identically to full name execution +exec launchr alias1 73 + +# Validate same conflict detection warning: +# Warning should appear regardless of execution method +stdout 'Action "test-print-input:full-2" was skipped:' +stdout 'alias "alias2" is already defined by "test-print-input:full-1"' + +# Validate successful alias execution: +# Should produce identical output to full name execution +stdout '^argInteger1: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: Conflicted Alias Execution +# ----------------------------------------------------------------------------- +# Execute using the conflicted alias (alias2) +# Should execute the first action since it claimed the alias first +exec launchr alias2 73 + +# Validate conflict detection warning: +# Should still warn about the skipped action +stdout 'Action "test-print-input:full-2" was skipped:' +stdout 'alias "alias2" is already defined by "test-print-input:full-1"' + +# Validate first action execution via conflicted alias: +# Should execute the first action, not the second +stdout '^argInteger1: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 4: Second Action Execution (Full Name) +# ----------------------------------------------------------------------------- +# Attempt to execute the second action using its full namespaced name +# This action should be skipped due to alias conflict +exec launchr test-print-input:full-2 73 + +# Validate conflict detection and skip message: +# Should show the action was skipped due to alias conflict +stdout 'Action "test-print-input:full-2" was skipped:' +stdout 'alias "alias2" is already defined by "test-print-input:full-1"' + +# Validate second action is NOT executed: +# Should not produce the second action's output +! stdout '^argInteger2: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 5: Second Action's Unique Alias Execution +# ----------------------------------------------------------------------------- +# Attempt to execute using the second action's unique alias (alias3) +# This should also be skipped since the entire action is invalid +exec launchr alias3 73 + +# Validate conflict detection and skip message: +# Should show the action was skipped due to alias conflict +stdout 'Action "test-print-input:full-2" was skipped:' +stdout 'alias "alias2" is already defined by "test-print-input:full-1"' + +# Validate second action is NOT executed: +# Should not produce the second action's output even via unique alias +! stdout '^argInteger2: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Actions with Conflicting Aliases +# ============================================================================= + +# First Action with Conflicting Aliases +-- test-print-input/actions/full-1/action.yaml -- +# Plugin action that claims aliases first (wins the conflict) +# This action should remain functional despite alias conflicts +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Action alias # Human-readable action name + + # Alias Configuration (First to claim): + # This action claims both aliases and should retain them + alias: + - alias1 # Unique alias (no conflict) + - alias2 # Conflicted alias (claimed first) + + # First Action Arguments: + # Uses argInteger1 to distinguish from second action + arguments: + - name: argInteger1 # Unique argument name + title: Argument Integer # Human-readable argument name + description: This is a required integer argument # Help text + type: integer # Expected type: integer + required: true # Must be provided by user + +# Second Action with Conflicting Aliases +-- test-print-input/actions/full-2/action.yaml -- +# Plugin action that attempts to claim already-used aliases +# This entire action should be skipped due to alias conflicts +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Action alias # Human-readable action name + + # Alias Configuration (Conflicting): + # This action attempts to claim aliases already used by full-1 + alias: + - alias2 # Conflicted alias (already claimed) + - alias3 # Unique alias (but action is skipped) + + # Second Action Arguments: + # Uses argInteger2 to distinguish from first action + arguments: + - name: argInteger2 # Different argument name + title: Argument Integer # Human-readable argument name + description: This is a required integer argument # Help text + type: integer # Expected type: integer + required: true # Must be provided by user + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Alias Conflict Resolution Rules: +# 1. Aliases must be unique across the entire action system +# 2. First action to define an alias wins the conflict +# 3. Subsequent actions with conflicting aliases are entirely skipped +# 4. Skipped actions cannot be executed by any method (full name or alias) +# 5. Warning messages are displayed for all conflicts detected +# +# Conflict Detection Behavior: +# - System scans all actions and builds alias registry +# - Conflicts are detected during action discovery phase +# - Conflicting actions are marked as skipped before execution +# - Warning messages identify specific conflicts and ownership +# +# Execution Behavior During Conflicts: +# - First action remains fully functional (full name + all aliases) +# - Second action becomes completely unavailable +# - Conflicted alias executes the first action that claimed it +# - Unique aliases of skipped actions are also unavailable +# +# Error Message Format: +# - Clear identification of skipped action +# - Specific alias causing the conflict +# - Reference to the action that owns the alias +# - Consistent messaging across all execution methods +# +# Action Skipping Strategy: +# - Entire action is skipped, not just conflicting aliases +# - Even unique aliases become unavailable when action is skipped +# - Full namespaced name execution is also blocked +# - No partial functionality is preserved for conflicted actions +# +# System Reliability: +# - Conflicts are handled gracefully without system errors +# - Warning messages provide clear debugging information +# - First-wins strategy ensures deterministic behavior +# - No stderr output ensures clean error handling +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/alias.txtar b/test/testdata/action/input/alias.txtar new file mode 100644 index 0000000..0286595 --- /dev/null +++ b/test/testdata/action/input/alias.txtar @@ -0,0 +1,139 @@ +# ============================================================================= +# Launchr Action Alias Functionality Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Execute actions using their full namespaced names +# 2. Execute actions using defined aliases +# 3. Handle multiple aliases for a single action +# 4. Reject undefined aliases with appropriate error messages +# 5. Pass arguments correctly through alias execution +# +# Test Focus: +# - Action alias definition and resolution +# - Argument passing through aliases +# - Error handling for undefined aliases +# - Full name vs. alias execution equivalence +# ============================================================================= + +# Test 1: Full Action Name Execution +# ----------------------------------------------------------------------------- +# Execute action using its complete namespaced name +# This serves as the baseline for alias functionality comparison +exec launchr test-print-input:full-name 73 + +# Validate successful execution with correct argument processing: +# Should output the integer argument with type information +stdout '^argInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: First Alias Execution +# ----------------------------------------------------------------------------- +# Execute action using its first defined alias +# Should produce identical output to full name execution +exec launchr alias1 73 + +# Validate alias execution produces same output as full name: +# Confirms alias correctly resolves to the target action +stdout '^argInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: Second Alias Execution +# ----------------------------------------------------------------------------- +# Execute action using its second defined alias +# Should produce identical output to full name execution +exec launchr alias2 73 + +# Validate second alias execution produces same output: +# Confirms multiple aliases can be defined for one action +stdout '^argInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 4: Undefined Alias Execution +# ----------------------------------------------------------------------------- +# Attempt to execute using an undefined alias +# Should fail with usage message rather than executing action +exec launchr alias3 73 + +# Validate undefined alias shows usage information: +# Should display help/usage instead of executing action +stdout 'Usage:' + +# Validate undefined alias does NOT execute the action: +# Should not produce the normal action output +! stdout '^argInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configuration with Aliases +# ============================================================================= + +# Action with Multiple Aliases +-- test-print-input/actions/full-name/action.yaml -- +# Plugin action demonstrating alias functionality +# This action can be executed using its full name or defined aliases +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Action alias # Human-readable action name + + # Alias Configuration: + # Defines alternative names for executing this action + alias: + - alias1 # First alias: short name + - alias2 # Second alias: alternative short name + # Note: alias3 is intentionally NOT defined to test error handling + + # Action Arguments: + # Required integer argument for testing parameter passing + arguments: + - name: argInteger # Argument identifier + title: Argument Integer # Human-readable argument name + description: This is a required integer argument # Help text + type: integer # Expected type: integer + required: true # Must be provided by user + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Alias Resolution Rules: +# 1. Actions can define multiple aliases for easier invocation +# 2. Aliases provide shorthand alternatives to full namespaced names +# 3. All defined aliases should execute the target action identically +# 4. Undefined aliases should show usage information, not execute +# 5. Arguments are passed through aliases without modification +# +# Execution Equivalence: +# - Full name execution: "test-print-input:full-name 73" +# - Alias execution: "alias1 73" or "alias2 73" +# - Both should produce identical output and behavior +# - All argument processing should work the same way +# +# Error Handling: +# - Undefined aliases (alias3) should not execute the action +# - Should display usage/help information instead +# - Should not produce normal action output +# - Should not generate error messages to stderr +# +# Alias Configuration: +# - Aliases are defined as a YAML array under 'alias' key +# - Each alias is a simple string identifier +# - Aliases should be unique across the system +# - Aliases provide convenient shortcuts for frequently used actions +# +# Argument Passing: +# - All arguments work identically through aliases +# - Required arguments must still be provided +# - Type validation applies equally to alias and full name execution +# - Argument processing is transparent to the execution method +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/args-opts.txtar b/test/testdata/action/input/args-opts.txtar new file mode 100644 index 0000000..e434c71 --- /dev/null +++ b/test/testdata/action/input/args-opts.txtar @@ -0,0 +1,224 @@ +# ============================================================================= +# Launchr Action Arguments and Options Integration Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Handle both positional arguments and named options in a single action +# 2. Parse mixed argument/option syntax correctly +# 3. Validate required arguments alongside optional options +# 4. Handle default values for arguments when options are present +# 5. Support flexible ordering of arguments and options +# 6. Validate option types and enum constraints +# 7. Provide clear error messages for validation failures +# +# Test Focus: +# - Mixed argument/option command-line parsing +# - Flexible parameter ordering +# - Type validation for both arguments and options +# - Default value handling in mixed scenarios +# - Error aggregation for multiple validation failures +# ============================================================================= + +# Test 1: Arguments Only (Minimal Valid Command) +# ----------------------------------------------------------------------------- +# Execute with only required argument, using default for optional argument +exec launchr test-print-input:args-opts 42 + +# Validate required argument processing: +# Should process the provided integer argument +stdout '^argInteger: 42 int true$' + +# Validate default value usage: +# Should use default value for optional argument with default +stdout '^argString: foo string false$' + +# Validate unspecified options: +# Should show nil for unspecified optional options +stdout '^optInteger: false$' +stdout '^optEnum: false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Arguments with Single Option +# ----------------------------------------------------------------------------- +# Execute with required argument and one named option +exec launchr test-print-input:args-opts 42 --optInteger 73 + +# Validate required argument processing: +# Should process the provided integer argument +stdout '^argInteger: 42 int true$' + +# Validate default value usage: +# Should still use default value for argument with default +stdout '^argString: foo string false$' + +# Validate option processing: +# Should process the provided integer option +stdout '^optInteger: 73 int true$' + +# Validate unspecified options: +# Should show nil for unspecified optional options +stdout '^optEnum: false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: Arguments with Option and Explicit Argument Value +# ----------------------------------------------------------------------------- +# Execute with both arguments provided and one named option +exec launchr test-print-input:args-opts 42 --optInteger 73 bar + +# Validate required argument processing: +# Should process the provided integer argument +stdout '^argInteger: 42 int true$' + +# Validate explicit argument value: +# Should use provided value instead of default +stdout '^argString: bar string true$' + +# Validate option processing: +# Should process the provided integer option +stdout '^optInteger: 73 int true$' + +# Validate unspecified options: +# Should show nil for unspecified optional options +stdout '^optEnum: false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 4: Mixed Ordering (Options Before Arguments) +# ----------------------------------------------------------------------------- +# Execute with flexible ordering: options mixed with arguments +exec launchr test-print-input:args-opts --optEnum enum1 42 bar --optInteger 73 + +# Validate argument parsing despite mixed ordering: +# Should correctly identify and process positional arguments +stdout '^argInteger: 42 int true$' +stdout '^argString: bar string true$' + +# Validate option processing despite mixed ordering: +# Should correctly process all named options regardless of position +stdout '^optInteger: 73 int true$' +stdout '^optEnum: enum1 string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 5: Multiple Validation Errors +# ----------------------------------------------------------------------------- +# Execute with multiple validation failures: missing argument and invalid enum +! exec launchr test-print-input:args-opts --optEnum enum3 + +# Validate missing argument error: +# Should report missing required argument +stdout '- \[arguments\]: missing property ''argInteger''' + +# Validate invalid enum option error: +# Should report invalid enum value with allowed options +stdout '- \[options optEnum\]: value must be one of ''enum1'', ''enum2''' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configuration with Arguments and Options +# ============================================================================= + +# Mixed Arguments and Options Action +-- test-print-input/actions/args-opts/action.yaml -- +# Plugin action demonstrating mixed arguments and options handling +# Shows how positional arguments and named options can coexist +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Arguments + options # Human-readable action name + + # Positional Arguments Section: + # Arguments are provided in order without flag names + arguments: + # Required integer argument (first positional parameter) + - name: argInteger # Argument identifier + title: Argument Integer # Human-readable argument name + description: This is a required integer argument # Help text + type: integer # Explicit integer type + required: true # Must be provided by user + + # Required string argument with default (second positional parameter) + - name: argString # Argument identifier + description: This is a required string argument with a default value # Help text + type: string # Explicit string type + required: true # Must be provided by user + default: "foo" # Default value when not provided + + # Named Options Section: + # Options are provided using --name syntax and can appear anywhere + options: + # Optional integer option + - name: optInteger # Option identifier + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + # Note: options are optional by default (no required: true) + + # Optional enum option + - name: optEnum # Option identifier + title: Option Enum # Human-readable option name + description: This is an optional string enum option # Help text + type: string # Base type: string + enum: [enum1, enum2] # Allowed values enumeration + # Note: options are optional by default (no required: true) + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Command-Line Parsing Rules: +# 1. Arguments are positional and parsed in declaration order +# 2. Options are named and can appear anywhere in the command line +# 3. Mixed ordering of arguments and options is supported +# 4. Arguments without values use their default values if available +# 5. Unspecified options show as nil in output +# +# Argument vs Option Distinctions: +# - Arguments: Positional, order-dependent, can have defaults +# - Options: Named with --, position-independent, always optional +# - Both support the same type system (integer, string, enum, etc.) +# - Both undergo the same validation processes +# +# Parsing Order Independence: +# - Options can appear before, after, or mixed with arguments +# - Parser correctly separates named options from positional arguments +# - Final values are determined by logical position, not command-line position +# +# Default Value Behavior: +# - Arguments with defaults use them when not explicitly provided +# - Options without values remain nil (no default mechanism) +# - Default usage is indicated in output (false for provided flag) +# +# Validation Integration: +# - Arguments and options are validated together +# - Multiple validation errors are collected and reported +# - JSON Schema validation applies to both arguments and options +# - Error messages clearly distinguish between argument and option errors +# +# Type System Consistency: +# - Same type validation rules apply to arguments and options +# - Integer, string, enum, and other types work identically +# - Format validation and constraints work the same way +# - Error messages maintain consistent formatting +# +# Error Message Format: +# - Arguments: [arguments] or [arguments argName] +# - Options: [options optName] +# - Clear separation between different validation failures +# - Specific error details for each validation type +# +# Usage Patterns: +# - Minimum: required arguments only +# - Common: required arguments + selected options +# - Maximum: all arguments + all options +# - Flexible: any combination with mixed ordering +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/args.txtar b/test/testdata/action/input/args.txtar new file mode 100644 index 0000000..3cd1fbf --- /dev/null +++ b/test/testdata/action/input/args.txtar @@ -0,0 +1,489 @@ +# ============================================================================= +# Launchr Action Argument Type Validation Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Handle different argument types (string, integer, number, boolean, enum, formatted) +# 2. Validate required arguments and provide appropriate error messages +# 3. Perform type-specific validation and conversion +# 4. Handle enum validation with restricted value sets +# 5. Validate formatted strings (email format) +# 6. Handle optional arguments with default values +# 7. Support multiple argument combinations +# +# Test Focus: +# - Argument type validation and conversion +# - Required argument enforcement +# - JSON Schema validation integration +# - Error message formatting and clarity +# - Default value handling for optional arguments +# ============================================================================= + +# ============================================================================= +# String Argument Tests +# ============================================================================= + +# Test 1: Valid String Argument +# ----------------------------------------------------------------------------- +# Provide 1 required string argument +exec launchr test-print-input:arg-string foo + +# Validate successful string argument processing: +# Should output the string value with type information +stdout '^argString: foo string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Missing String Argument +# ----------------------------------------------------------------------------- +# Have error when argument missing +! exec launchr test-print-input:arg-string + +# Validate missing argument error message: +# Should show JSON Schema validation error for missing required property +stdout '- \[arguments\]: missing property ''argString''' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Integer Argument Tests +# ============================================================================= + +# Test 3: Valid Integer Argument +# ----------------------------------------------------------------------------- +# Provide 1 required integer argument +exec launchr test-print-input:arg-integer 42 + +# Validate successful integer argument processing: +# Should output the integer value with type information +stdout '^argInt: 42 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 4: Invalid Integer Argument +# ----------------------------------------------------------------------------- +# Have error when argument is not an integer +! exec launchr test-print-input:arg-integer foo + +# Validate integer parsing error message: +# Should show Go strconv parsing error for invalid integer +# TODO Change error in code. +stdout 'strconv\.Atoi: parsing "foo": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 5: Missing Integer Argument +# ----------------------------------------------------------------------------- +# Have error when argument is missing +! exec launchr test-print-input:arg-integer + +# Validate missing argument error message: +# Should show JSON Schema validation error for missing required property +stdout '- \[arguments\]: missing property ''argInt''' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Number (Float) Argument Tests +# ============================================================================= + +# Test 6: Valid Number Argument +# ----------------------------------------------------------------------------- +# Provide 1 required number argument +exec launchr test-print-input:arg-number 37.73 + +# Validate successful number argument processing: +# Should output the float value with type information +stdout '^argNumber: 37\.73 float64 true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 7: Invalid Number Argument (Locale Format) +# ----------------------------------------------------------------------------- +# Have error when argument is not a number +! exec launchr test-print-input:arg-number 37,73 + +# Validate number parsing error message: +# Should show Go strconv parsing error for invalid number format +# TODO Change error in code. +stdout 'strconv\.ParseFloat: parsing "37,73": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 8: Missing Number Argument +# ----------------------------------------------------------------------------- +# Have error when argument is missing +! exec launchr test-print-input:arg-number + +# Validate missing argument error message: +# Should show JSON Schema validation error for missing required property +stdout '- \[arguments\]: missing property ''argNumber''' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Enum Argument Tests +# ============================================================================= + +# Test 9: Valid Enum Argument +# ----------------------------------------------------------------------------- +# Provide 1 required string enum argument +exec launchr test-print-input:arg-enum enum2 + +# Validate successful enum argument processing: +# Should output the enum value with type information +stdout '^argEnum: enum2 string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 10: Invalid Enum Argument +# ----------------------------------------------------------------------------- +# Have error when argument is not correct enum +! exec launchr test-print-input:arg-enum badEnum + +# Validate enum validation error message: +# Should show JSON Schema validation error with allowed enum values +stdout '- \[arguments argEnum\]: value must be one of ''enum1'', ''enum2''' + +# Validate clean execution (no error output) +! stderr . + +# Test 11: Missing Enum Argument +# ----------------------------------------------------------------------------- +# Have error when argument is missing +! exec launchr test-print-input:arg-enum + +# Validate missing argument error message: +# Should show JSON Schema validation error for missing required property +stdout '- \[arguments\]: missing property ''argEnum''' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Boolean Argument Tests +# ============================================================================= + +# Test 12-17: Valid Boolean Arguments (Various Formats) +# ----------------------------------------------------------------------------- +# Provide 1 required boolean argument - numeric true +exec launchr test-print-input:arg-boolean 1 +stdout '^argBoolean: true bool true$' +! stderr . + +# Provide 1 required boolean argument - capitalized true +exec launchr test-print-input:arg-boolean True +stdout '^argBoolean: true bool true$' +! stderr . + +# Provide 1 required boolean argument - lowercase true +exec launchr test-print-input:arg-boolean true +stdout '^argBoolean: true bool true$' +! stderr . + +# Provide 1 required boolean argument - numeric false +exec launchr test-print-input:arg-boolean 0 +stdout '^argBoolean: false bool true$' +! stderr . + +# Provide 1 required boolean argument - capitalized false +exec launchr test-print-input:arg-boolean False +stdout '^argBoolean: false bool true$' +! stderr . + +# Provide 1 required boolean argument - lowercase false +exec launchr test-print-input:arg-boolean false +stdout '^argBoolean: false bool true$' +! stderr . + +# Test 18: Invalid Boolean Argument +# ----------------------------------------------------------------------------- +# Have error when argument is not correct boolean +! exec launchr test-print-input:arg-boolean no + +# Validate boolean parsing error message: +# Should show Go strconv parsing error for invalid boolean +# TODO Change error in code. +stdout 'strconv\.ParseBool: parsing "no": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 19: Missing Boolean Argument +# ----------------------------------------------------------------------------- +# Have error when argument is missing +! exec launchr test-print-input:arg-boolean + +# Validate missing argument error message: +# Should show JSON Schema validation error for missing required property +stdout '- \[arguments\]: missing property ''argBoolean''' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Formatted String Argument Tests (Email) +# ============================================================================= + +# Test 20: Valid Email Argument +# ----------------------------------------------------------------------------- +# Provide 1 required formatted string argument +exec launchr test-print-input:arg-email foo@example.com + +# Validate successful email argument processing: +# Should output the email value with type information +stdout '^argEmail: foo@example.com string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 21: Invalid Email Argument +# ----------------------------------------------------------------------------- +# Have error when argument is not an integer +! exec launchr test-print-input:arg-email foo + +# Validate email format validation error message: +# Should show specific email validation error +stdout '- \[arguments argEmail\]: ''foo'' is not valid email: missing @' + +# Validate clean execution (no error output) +! stderr . + +# Test 22: Missing Email Argument +# ----------------------------------------------------------------------------- +# Have error when argument is missing +! exec launchr test-print-input:arg-email + +# Validate missing argument error message: +# Should show JSON Schema validation error for missing required property +stdout '- \[arguments\]: missing property ''argEmail''' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Multiple Arguments Tests +# ============================================================================= + +# Test 23: All Three Arguments Provided +# ----------------------------------------------------------------------------- +# Provide 3 arguments: 1 required, 2 optional with a default value, 3 optional +exec launchr test-print-input:args-3 42 bar enum1 + +# Validate all three arguments processed correctly: +# Should output all three values with their type information +stdout '^argInteger: 42 int true$' +stdout '^argString: bar string true$' +stdout '^argEnum: enum1 string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 24: Two Arguments Provided (Third Optional Missing) +# ----------------------------------------------------------------------------- +# Provide 3 arguments: 1 required, 2 optional with a default value, no 3rd +exec launchr test-print-input:args-3 42 bar + +# Validate two arguments processed, third shows as nil: +# Should output first two values and nil for missing optional +stdout '^argInteger: 42 int true$' +stdout '^argString: bar string true$' +stdout '^argEnum: false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 25: One Argument Provided (Default Value Used) +# ----------------------------------------------------------------------------- +# Provide 3 arguments, 1 required, 2 optional with a default value, no 3rd +exec launchr test-print-input:args-3 42 + +# Validate required argument and default value usage: +# Should output required argument, default value, and nil for missing optional +stdout '^argInteger: 42 int true$' +stdout '^argString: foo string false$' +stdout '^argEnum: false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configurations for Different Argument Types +# ============================================================================= + +# String Argument Action +-- test-print-input/actions/arg-string/action.yaml -- +# Plugin action demonstrating string argument handling +# String type is implicit when no type is specified +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Argument String # Human-readable action name + + arguments: + - name: argString # Argument identifier + description: This is a required implicit string argument # Help text + title: Argument String # Human-readable argument name + required: true # Must be provided by user + # Note: type defaults to string when not specified + +# Integer Argument Action +-- test-print-input/actions/arg-integer/action.yaml -- +# Plugin action demonstrating integer argument handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Argument Array Integer # Human-readable action name + + arguments: + - name: argInt # Argument identifier + title: Argument Integer # Human-readable argument name + description: This is a required integer argument # Help text + type: integer # Explicit integer type + required: true # Must be provided by user + +# Number (Float) Argument Action +-- test-print-input/actions/arg-number/action.yaml -- +# Plugin action demonstrating number (float) argument handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Argument Number # Human-readable action name + + arguments: + - name: argNumber # Argument identifier + title: Argument number # Human-readable argument name + description: This is a required float argument with a default value # Help text + type: number # Explicit number (float64) type + required: true # Must be provided by user + +# Boolean Argument Action +-- test-print-input/actions/arg-boolean/action.yaml -- +# Plugin action demonstrating boolean argument handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Argument Boolean # Human-readable action name + + arguments: + - name: argBoolean # Argument identifier + title: Argument Boolean # Human-readable argument name + type: boolean # Explicit boolean type + description: This is a required boolean argument # Help text + required: true # Must be provided by user + +# Enum Argument Action +-- test-print-input/actions/arg-enum/action.yaml -- +# Plugin action demonstrating enum argument handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Argument String enum # Human-readable action name + + arguments: + - name: argEnum # Argument identifier + title: Argument Enum # Human-readable argument name + description: This is an required string enum argument # Help text + type: string # Base type: string + enum: [enum1, enum2] # Allowed values enumeration + required: true # Must be provided by user + +# Email Format Argument Action +-- test-print-input/actions/arg-email/action.yaml -- +# Plugin action demonstrating formatted string argument handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Argument String Format # Human-readable action name + + arguments: + - name: argEmail # Argument identifier + title: Argument Email # Human-readable argument name + description: This is a required formatted argument # Help text + type: string # Base type: string + format: email # Format validation: email + required: true # Must be provided by user + +# Multiple Arguments Action +-- test-print-input/actions/args-3/action.yaml -- +# Plugin action demonstrating multiple argument handling with defaults +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - 3 arguments # Human-readable action name + + arguments: + # Required integer argument (no default) + - name: argInteger # Argument identifier + title: Argument Integer # Human-readable argument name + description: This is a required integer argument # Help text + type: integer # Explicit integer type + required: true # Must be provided by user + + # Required string argument with default value + - name: argString # Argument identifier + description: This is a required string argument with a default value # Help text + type: string # Explicit string type + required: true # Must be provided by user + default: "foo" # Default value when not provided + + # Optional enum argument (no default) + - name: argEnum # Argument identifier + title: Argument Enum # Human-readable argument name + description: This is an optional string enum argument # Help text + type: string # Base type: string + enum: [enum1, enum2] # Allowed values enumeration + # Note: required defaults to false for optional arguments + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Argument Type Support: +# 1. String: Implicit default type, accepts any text input +# 2. Integer: Explicit type, validates and converts to int +# 3. Number: Explicit type, validates and converts to float64 +# 4. Boolean: Explicit type, accepts various true/false formats +# 5. Enum: String type with restricted value set +# 6. Formatted: String type with format validation (email, etc.) +# +# Validation Rules: +# - Required arguments must be provided or have default values +# - Type validation occurs before action execution +# - Invalid types show specific parsing errors +# - Missing required arguments show JSON Schema errors +# - Enum validation shows allowed values in error messages +# +# Error Message Types: +# - JSON Schema validation errors for missing/invalid properties +# - Go strconv parsing errors for type conversion failures +# - Format-specific validation errors for formatted strings +# - Clear identification of argument names and expected values +# +# Default Value Behavior: +# - Default values are used when arguments are not provided +# - Required arguments can have default values +# - Optional arguments without defaults show as nil +# - Default values are indicated in output (false for provided flag) +# +# Boolean Format Support: +# - Numeric: 1 (true), 0 (false) +# - Text: true/false, True/False (case variations) +# - Invalid formats like "yes/no" are rejected +# +# Number Format Requirements: +# - Standard JSON/YAML numeric format required +# - Dot (.) decimal separator, not comma (,) +# - Scientific notation supported +# - Locale-specific formatting rejected +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/opts.txtar b/test/testdata/action/input/opts.txtar new file mode 100644 index 0000000..e0de692 --- /dev/null +++ b/test/testdata/action/input/opts.txtar @@ -0,0 +1,997 @@ +# ============================================================================= +# Launchr Action Options Type Validation Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Handle different option types (string, integer, number, boolean, enum, array) +# 2. Support optional and required options with default values +# 3. Validate option types and provide appropriate error messages +# 4. Handle array options with various item types +# 5. Validate formatted strings (IP addresses) +# 6. Support multiple option syntax formats (--flag, --flag=value) +# 7. Handle default value validation and type checking +# 8. Provide clear error messages for validation failures +# +# Test Focus: +# - Named option parsing and validation +# - Array option handling with type constraints +# - Default value behavior and validation +# - Required option enforcement +# - Format validation for specialized string types +# - Boolean option flag syntax variations +# ============================================================================= + +# ============================================================================= +# String Option Tests +# ============================================================================= + +# Test 1: Valid String Option +# ----------------------------------------------------------------------------- +# Provide 1 optional string option +exec launchr test-print-input:opt-string --optString bar + +# Validate successful string option processing: +# Should output the string value with type information +stdout '^optString: bar string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Missing String Option with Default +# ----------------------------------------------------------------------------- +# No error when optional option is missing +exec launchr test-print-input:opt-string + +# Validate default value usage: +# Should use default value when option is not provided +stdout '^optString: foo string false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Required Option Tests +# ============================================================================= + +# Test 3: Missing Required Option +# ----------------------------------------------------------------------------- +# Have error when required option is missing +! exec launchr test-print-input:opt-required + +# Validate missing required option error: +# Should show JSON Schema validation error for missing required option +stdout '- \[options\]: missing property ''optString''' + +# Validate clean execution (no error output) +! stderr . + +# Test 4: Valid Required Option with Default +# ----------------------------------------------------------------------------- +# Provide 1 required string option with default value +exec launchr test-print-input:opt-required-default --optString bar + +# Validate successful required option processing: +# Should output the provided string value +stdout '^optString: bar string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 5: Missing Required Option with Default +# ----------------------------------------------------------------------------- +# Use default value when required option with default is missing +! exec launchr test-print-input:opt-required-default + +# Validate CLI framework error for missing required flag: +# Should show command-line parsing error for missing required flag +stdout 'required flag\(s\) "optString" not set' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Integer Option Tests +# ============================================================================= + +# Test 6: Valid Integer Option +# ----------------------------------------------------------------------------- +# Provide 1 optional integer option +exec launchr test-print-input:opt-integer --optInt 42 + +# Validate successful integer option processing: +# Should output the integer value with type information +stdout '^optInt: 42 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 7: Invalid Integer Option +# ----------------------------------------------------------------------------- +# Have error when option is not an integer +! exec launchr test-print-input:opt-integer --optInt foo + +# Validate integer parsing error: +# Should show Go strconv parsing error for invalid integer +# TODO Change error in code. +stdout 'strconv\.ParseInt: parsing "foo": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 8: Missing Integer Option with Default +# ----------------------------------------------------------------------------- +# No error when optional integer option is missing +exec launchr test-print-input:opt-integer + +# Validate default value usage: +# Should use default value when option is not provided +stdout '^optInt: 42 int false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Number (Float) Option Tests +# ============================================================================= + +# Test 9: Valid Number Option +# ----------------------------------------------------------------------------- +# Provide 1 optional number option +exec launchr test-print-input:opt-number --optNumber 73.37 + +# Validate successful number option processing: +# Should output the float value with type information +stdout '^optNumber: 73\.37 float64 true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 10: Invalid Number Option (Locale Format) +# ----------------------------------------------------------------------------- +# Have error when option is not a number +! exec launchr test-print-input:opt-number --optNumber 73,37 + +# Validate number parsing error: +# Should show Go strconv parsing error for invalid number format +# TODO Change error in code. +stdout 'strconv\.ParseFloat: parsing "73,37": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 11: Missing Number Option with Default +# ----------------------------------------------------------------------------- +# No error when optional number option is missing +exec launchr test-print-input:opt-number + +# Validate default value usage: +# Should use default value when option is not provided +stdout '^optNumber: 37.73 float64 false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Enum Option Tests +# ============================================================================= + +# Test 12: Valid Enum Option +# ----------------------------------------------------------------------------- +# Provide 1 optional int enum option +exec launchr test-print-input:opt-enum --optEnum 73 + +# Validate successful enum option processing: +# Should output the enum value with type information +stdout '^optEnum: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 13: Invalid Enum Option +# ----------------------------------------------------------------------------- +# Have error when option is not correct enum +! exec launchr test-print-input:opt-enum --optEnum 99 + +# Validate enum validation error: +# Should show JSON Schema validation error with allowed enum values +stdout '- \[options optEnum\]: value must be one of 37, 73' + +# Validate clean execution (no error output) +! stderr . + +# Test 14: Missing Enum Option with Default +# ----------------------------------------------------------------------------- +# No error when optional enum option is missing +exec launchr test-print-input:opt-enum + +# Validate default value usage: +# Should use default value when option is not provided +stdout '^optEnum: 37 int false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 15: Valid Enum Option with Incorrect Default +# ----------------------------------------------------------------------------- +# No error when optional enum option is missing +exec launchr test-print-input:opt-enum-incorrect-default --optEnum 37 + +# Validate successful enum option processing: +# Should work when valid value is provided +stdout '^optEnum: 37 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 16: Missing Enum Option with Invalid Default +# ----------------------------------------------------------------------------- +# No error when optional enum option is missing +! exec launchr test-print-input:opt-enum-incorrect-default + +# Validate default value validation error: +# Should show validation error for invalid default value +stdout '- \[options optEnum\]: value must be one of 37, 73' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Boolean Option Tests +# ============================================================================= + +# Test 17-21: Valid Boolean Options (Various Formats) +# ----------------------------------------------------------------------------- +# Provide 1 optional boolean option - flag syntax +exec launchr test-print-input:opt-boolean --optBoolean +stdout '^optBoolean: true bool true$' +! stderr . + +# Provide 1 optional boolean option - numeric true +exec launchr test-print-input:opt-boolean --optBoolean=1 +stdout '^optBoolean: true bool true$' +! stderr . + +# Provide 1 optional boolean option - capitalized true +exec launchr test-print-input:opt-boolean --optBoolean=True +stdout '^optBoolean: true bool true$' +! stderr . + +# Provide 1 optional boolean option - numeric false +exec launchr test-print-input:opt-boolean --optBoolean=0 +stdout '^optBoolean: false bool true$' +! stderr . + +# Provide 1 optional boolean option - capitalized false +exec launchr test-print-input:opt-boolean --optBoolean=False +stdout '^optBoolean: false bool true$' +! stderr . + +# Test 22: Invalid Boolean Option +# ----------------------------------------------------------------------------- +# Have error when option is not correct boolean +! exec launchr test-print-input:opt-boolean --optBoolean=no + +# Validate boolean parsing error: +# Should show Go strconv parsing error for invalid boolean +# TODO Change error in code. +stdout 'strconv\.ParseBool: parsing "no": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 23: Missing Boolean Option with Default +# ----------------------------------------------------------------------------- +# No error when optional boolean option is missing +exec launchr test-print-input:opt-boolean + +# Validate default value usage: +# Should use default value when option is not provided +stdout '^optBoolean: true bool false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Formatted String Option Tests (IP Address) +# ============================================================================= + +# Test 24: Valid IP Option +# ----------------------------------------------------------------------------- +# Provide 1 optional formatted IP option +exec launchr test-print-input:opt-ip --optIP 192.168.1.1 + +# Validate successful IP option processing: +# Should output the IP address with type information +stdout '^optIP: 192\.168\.1\.1 string true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 25: Invalid IP Option +# ----------------------------------------------------------------------------- +# Have error when option is not a valid IP +! exec launchr test-print-input:opt-ip --optIP 999.999.999.999 + +# Validate IP format validation error: +# Should show specific IP validation error +stdout '- \[options optIP\]: ''999\.999\.999\.999'' is not valid ipv4' + +# Validate clean execution (no error output) +! stderr . + +# Test 26: Missing IP Option with Default +# ----------------------------------------------------------------------------- +# No error when optional IP option is missing +exec launchr test-print-input:opt-ip + +# Validate default value usage: +# Should use default IP address when option is not provided +stdout '^optIP: 1\.1\.1\.1 string false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Array Option Tests - String Arrays +# ============================================================================= + +# Test 27: String Array Option (Single Value) +# ----------------------------------------------------------------------------- +# Provide 1 optional string array option (single value) +exec launchr test-print-input:opt-array-string --optArrayString foo + +# Validate successful string array option processing: +# Should output array with single string value +stdout '^optArrayString: \[foo\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 28: String Array Option (Multiple Values) +# ----------------------------------------------------------------------------- +# Provide 1 optional string array option (multiple values) +exec launchr test-print-input:opt-array-string --optArrayString foo --optArrayString bar --optArrayString=bar,buz + +# Validate successful multiple string array processing: +# Should output array with all provided values (including comma-separated) +stdout '^optArrayString: \[foo bar bar buz\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 29: Missing String Array Option with Default +# ----------------------------------------------------------------------------- +# No error when optional string array option is missing +exec launchr test-print-input:opt-array-string + +# Validate default array value usage: +# Should use default array when option is not provided +stdout '^optArrayString: \[foo bar\] \[\]interface {} false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Array Option Tests - Integer Arrays +# ============================================================================= + +# Test 30: Integer Array Option (Single Value) +# ----------------------------------------------------------------------------- +# Provide 1 optional integer array option (single value) +exec launchr test-print-input:opt-array-integer --optArrayInteger 42 + +# Validate successful integer array option processing: +# Should output array with single integer value +stdout '^optArrayInteger: \[42\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 31: Integer Array Option (Multiple Values) +# ----------------------------------------------------------------------------- +# Provide 1 optional integer array option (multiple values) +exec launchr test-print-input:opt-array-integer --optArrayInteger 42 --optArrayInteger 24 + +# Validate successful multiple integer array processing: +# Should output array with all provided integer values +stdout '^optArrayInteger: \[42 24\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 32: Invalid Integer Array Option +# ----------------------------------------------------------------------------- +# Have error when array option contains non-integer +! exec launchr test-print-input:opt-array-integer --optArrayInteger foo + +# Validate integer array parsing error: +# Should show Go strconv parsing error for invalid integer in array +# TODO Change error in code. +stdout 'strconv\.Atoi: parsing "foo": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 33: Missing Integer Array Option with Default +# ----------------------------------------------------------------------------- +# No error when optional integer array option is missing +exec launchr test-print-input:opt-array-integer + +# Validate default array value usage: +# Should use default integer array when option is not provided +stdout '^optArrayInteger: \[37 73\] \[\]interface {} false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Array Option Tests - Number Arrays +# ============================================================================= + +# Test 34: Number Array Option (Single Value) +# ----------------------------------------------------------------------------- +# Provide 1 optional number array option (single value) +exec launchr test-print-input:opt-array-number --optArrayNumber 37.73 + +# Validate successful number array option processing: +# Should output array with single number value +stdout '^optArrayNumber: \[37\.73\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 35: Number Array Option (Multiple Values) +# ----------------------------------------------------------------------------- +# Provide 1 optional number array option (multiple values) +exec launchr test-print-input:opt-array-number --optArrayNumber 37.73 --optArrayNumber 42.5 --optArrayNumber=37.37,73.73 + +# Validate successful multiple number array processing: +# Should output array with all provided number values (including comma-separated) +stdout '^optArrayNumber: \[37\.73 42\.5\ 37\.37 73\.73] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 36: Invalid Number Array Option +# ----------------------------------------------------------------------------- +# Have error when array option contains non-number +! exec launchr test-print-input:opt-array-number --optArrayNumber 37-73 + +# Validate number array parsing error: +# Should show Go strconv parsing error for invalid number in array +# TODO Change error in code. +stdout 'strconv\.ParseFloat: parsing "37-73": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 37: Missing Number Array Option with Default +# ----------------------------------------------------------------------------- +# No error when optional number array option is missing +exec launchr test-print-input:opt-array-number + +# Validate default array value usage: +# Should use default number array when option is not provided +stdout '^optArrayNumber: \[37\.73 73\.37\] \[\]interface {} false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 38: Number Array Option with Incorrect Default Type +# ----------------------------------------------------------------------------- +# No error when optional number array option is missing +! exec launchr test-print-input:opt-array-number-incorrect-default + +# Validate default value type validation error: +# Should show validation error for incorrect default array item type +stdout '- \[options optArrayNumber 0\]: got string, want number' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Array Option Tests - Boolean Arrays +# ============================================================================= + +# Test 39: Boolean Array Option (Single Value) +# ----------------------------------------------------------------------------- +# Provide 1 optional boolean array option (single value) +exec launchr test-print-input:opt-array-boolean --optArrayBoolean true + +# Validate successful boolean array option processing: +# Should output array with single boolean value +stdout '^optArrayBoolean: \[true\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 40: Boolean Array Option (Multiple Values) +# ----------------------------------------------------------------------------- +# Provide 1 optional boolean array option (multiple values) +exec launchr test-print-input:opt-array-boolean --optArrayBoolean false --optArrayBoolean true --optArrayBoolean=true,false + +# Validate successful multiple boolean array processing: +# Should output array with all provided boolean values (including comma-separated) +stdout '^optArrayBoolean: \[false true true false\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 41: Boolean Array Option Missing Value +# ----------------------------------------------------------------------------- +# Provide 1 optional boolean array option (multiple values) +! exec launchr test-print-input:opt-array-boolean --optArrayBoolean + +# Validate boolean array requires explicit value: +# Should show error that boolean array flag needs an argument +stdout 'flag needs an argument: --optArrayBoolean' + +# Validate clean execution (no error output) +! stderr . + +# Test 42: Invalid Boolean Array Option +# ----------------------------------------------------------------------------- +# Have error when array option contains non-boolean +! exec launchr test-print-input:opt-array-boolean --optArrayBoolean maybe + +# Validate boolean array parsing error: +# Should show Go strconv parsing error for invalid boolean in array +# TODO Change error in code. +stdout 'strconv\.ParseBool: parsing "maybe": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 43: Missing Boolean Array Option with Default +# ----------------------------------------------------------------------------- +# No error when optional boolean array option is missing +exec launchr test-print-input:opt-array-boolean + +# Validate default array value usage: +# Should use default boolean array when option is not provided +stdout '^optArrayBoolean: \[true false\] \[\]interface {} false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Array Option Tests - Enum Arrays +# ============================================================================= + +# Test 44: Enum Array Option (Single Value) +# ----------------------------------------------------------------------------- +# Provide 1 optional enum array option (single value) +exec launchr test-print-input:opt-array-enum --optArrayEnum 37 + +# Validate successful enum array option processing: +# Should output array with single enum value +stdout '^optArrayEnum: \[37\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 45: Enum Array Option (Multiple Values) +# ----------------------------------------------------------------------------- +# Provide 1 optional enum array option (multiple values) +exec launchr test-print-input:opt-array-enum --optArrayEnum 37 --optArrayEnum 73 --optArrayEnum=37,73 + +# Validate successful multiple enum array processing: +# Should output array with all provided enum values (including comma-separated) +stdout '^optArrayEnum: \[37 73 37 73\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 46: Invalid Enum Array Option (Invalid Enum Value) +# ----------------------------------------------------------------------------- +# Have error when array option contains invalid enum value +! exec launchr test-print-input:opt-array-enum --optArrayEnum 42 + +# Validate enum array validation error: +# Should show JSON Schema validation error for invalid enum value in array +stdout '- \[options optArrayEnum 0\]: value must be one of 37, 73' + +# Validate clean execution (no error output) +! stderr . + +# Test 47: Invalid Enum Array Option (Invalid Type) +# ----------------------------------------------------------------------------- +# Have error when array option contains invalid enum value +! exec launchr test-print-input:opt-array-enum --optArrayEnum badEnum + +# Validate enum array parsing error: +# Should show Go strconv parsing error for invalid type in enum array +stdout 'strconv\.Atoi: parsing "badEnum": invalid syntax' + +# Validate clean execution (no error output) +! stderr . + +# Test 48: Missing Enum Array Option with Default +# ----------------------------------------------------------------------------- +# No error when optional enum array option is missing +exec launchr test-print-input:opt-array-enum + +# Validate default array value usage: +# Should use default enum array when option is not provided +stdout '^optArrayEnum: \[37 73\] \[\]interface {} false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Multiple Options Tests +# ============================================================================= + +# Test 49: All Three Options Provided +# ----------------------------------------------------------------------------- +# Provide 3 options +exec launchr test-print-input:opts-3 --optInteger=73 --optString=bar --optArray=buz + +# Validate all three options processed correctly: +# Should output all three option values with their type information +stdout '^optInteger: 73 int true$' +stdout '^optString: bar string true$' +stdout '^optArray: \[buz\] \[\]interface {} true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 50: One Option Provided (Others Use Defaults) +# ----------------------------------------------------------------------------- +# Provide 3 arguments: 1 required, 2 optional with a default value, no 3rd +exec launchr test-print-input:opts-3 --optInteger=73 + +# Validate one option processed, others use defaults: +# Should output provided option and default values for others +stdout '^optInteger: 73 int true$' +stdout '^optString: foo string false' +stdout '^optArray: \[\] \[\]interface {} false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 51: No Options Provided (All Use Defaults) +# ----------------------------------------------------------------------------- +# Provide 3 arguments, 1 required, 2 optional with a default value, no 3rd +exec launchr test-print-input:opts-3 + +# Validate all options use defaults or show as nil: +# Should output default values or nil for all options +stdout '^optInteger: false' +stdout '^optString: foo string false$' +stdout '^optArray: \[\] \[\]interface {} false$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configurations for Different Option Types +# ============================================================================= + +# String Option Action +-- test-print-input/actions/opt-string/action.yaml -- +# Plugin action demonstrating string option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option String # Human-readable action name + + options: + - name: optString # Option identifier + description: This is an optional string option # Help text + title: Option String # Human-readable option name + type: string # Explicit string type + default: foo # Default value when not provided + +# Required String Option Action (No Default) +-- test-print-input/actions/opt-required/action.yaml -- +# Plugin action demonstrating required string option without default +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option String Required No default # Human-readable action name + + options: + - name: optString # Option identifier + description: This is a required string option without a default value. # Help text + title: Option String # Human-readable option name + type: string # Explicit string type + required: true # Must be provided by user + +# Required String Option Action (With Default) +-- test-print-input/actions/opt-required-default/action.yaml -- +# Plugin action demonstrating required string option with default +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option String Required Default # Human-readable action name + + options: + - name: optString # Option identifier + description: This is a required string option with a default value # Help text + title: Option String # Human-readable option name + type: string # Explicit string type + default: foo # Default value when not provided + required: true # Must be provided by user + +# Integer Option Action +-- test-print-input/actions/opt-integer/action.yaml -- +# Plugin action demonstrating integer option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Integer # Human-readable action name + + options: + - name: optInt # Option identifier + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + default: 42 # Default value when not provided + +# Number Option Action +-- test-print-input/actions/opt-number/action.yaml -- +# Plugin action demonstrating number (float) option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Number # Human-readable action name + + options: + - name: optNumber # Option identifier + title: Option number # Human-readable option name + description: This is an optional float option # Help text + type: number # Explicit number (float64) type + default: 37.73 # Default value when not provided + +# Enum Option Action +-- test-print-input/actions/opt-enum/action.yaml -- +# Plugin action demonstrating enum option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Integer enum # Human-readable action name + + options: + - name: optEnum # Option identifier + title: Option Enum Integer # Human-readable option name + description: This is an optional integer enum option # Help text + type: integer # Base type: integer + enum: [37, 73] # Allowed values enumeration + default: 37 # Default value when not provided + +# Enum Option Action with Invalid Default +-- test-print-input/actions/opt-enum-incorrect-default/action.yaml -- +# Plugin action demonstrating enum validation of default values +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Integer enum # Human-readable action name + + options: + - name: optEnum # Option identifier + title: Option Enum Integer # Human-readable option name + description: This is an optional integer enum option # Help text + type: integer # Base type: integer + enum: [37, 73] # Allowed values enumeration + default: 99 # Invalid default value (not in enum) + +# Boolean Option Action +-- test-print-input/actions/opt-boolean/action.yaml -- +# Plugin action demonstrating boolean option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Boolean # Human-readable action name + + options: + - name: optBoolean # Option identifier + title: Option Boolean # Human-readable option name + type: boolean # Explicit boolean type + description: This is an optional boolean option # Help text + default: true # Default value when not provided + +# IP Address Option Action +-- test-print-input/actions/opt-ip/action.yaml -- +# Plugin action demonstrating formatted string option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option String Format # Human-readable action name + + options: + - name: optIP # Option identifier + title: Option IP # Human-readable option name + description: This is an optional formatted option # Help text + type: string # Base type: string + format: ipv4 # Format validation: IPv4 address + default: 1.1.1.1 # Default IP address + +# String Array Option Action +-- test-print-input/actions/opt-array-string/action.yaml -- +# Plugin action demonstrating string array option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Array String # Human-readable action name + + options: + - name: optArrayString # Option identifier + title: Option Array String # Human-readable option name + type: array # Array type + description: This is an optional array option # Help text + default: ["foo", "bar"] # Default array values + # Note: items type defaults to string for arrays + +# Integer Array Option Action +-- test-print-input/actions/opt-array-integer/action.yaml -- +# Plugin action demonstrating integer array option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Array Integer # Human-readable action name + + options: + - name: optArrayInteger # Option identifier + title: Option Array Integer # Human-readable option name + type: array # Array type + description: This is a optional array option # Help text + default: [37, 73] # Default array values + items: + type: integer # Array item type: integer + +# Number Array Option Action +-- test-print-input/actions/opt-array-number/action.yaml -- +# Plugin action demonstrating number array option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Array Number # Human-readable action name + + options: + - name: optArrayNumber # Option identifier + title: Option Array Number # Human-readable option name + type: array # Array type + description: This is a optional array option # Help text + default: [37.73, 73.37] # Default array values + items: + type: number # Array item type: number + +# Number Array Option Action with Invalid Default +-- test-print-input/actions/opt-array-number-incorrect-default/action.yaml -- +# Plugin action demonstrating array default value validation +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Array Number # Human-readable action name + + options: + - name: optArrayNumber # Option identifier + title: Option Array Number # Human-readable option name + type: array # Array type + description: This is a optional array option # Help text + default: ["37,73"] # Invalid default (string instead of number) + items: + type: number # Array item type: number + +# Enum Array Option Action +-- test-print-input/actions/opt-array-enum/action.yaml -- +# Plugin action demonstrating enum array option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Array Integer Enum # Human-readable action name + + options: + - name: optArrayEnum # Option identifier + title: Option Array Enum # Human-readable option name + type: array # Array type + description: This is a optional array option # Help text + default: [37, 73] # Default array values + items: + type: integer # Array item type: integer + enum: [37, 73] # Allowed values for each array item + +# Boolean Array Option Action +-- test-print-input/actions/opt-array-boolean/action.yaml -- +# Plugin action demonstrating boolean array option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - Option Array Boolean # Human-readable action name + + options: + - name: optArrayBoolean # Option identifier + title: Option Array Boolean # Human-readable option name + type: array # Array type + description: This is a optional array option # Help text + default: [true, false] # Default array values + items: + type: boolean # Array item type: boolean + +# Multiple Options Action +-- test-print-input/actions/opts-3/action.yaml -- +# Plugin action demonstrating multiple option handling +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input type - 3 options # Human-readable action name + + options: + # Optional integer option (no default) + - name: optInteger # Option identifier + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + # Note: no default value, will show as nil + + # Optional string option with default + - name: optString # Option identifier + description: This is an optional string option with a default value # Help text + type: string # Explicit string type + default: "foo" # Default value when not provided + + # Optional array option (no default) + - name: optArray # Option identifier + title: Option Array # Human-readable option name + description: This is an optional array option # Help text + type: array # Array type + # Note: no default value, will show as empty array + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Option Type Support: +# 1. String: Basic text input with optional default values +# 2. Integer: Numeric input with validation and conversion to int +# 3. Number: Floating-point input with validation and conversion to float64 +# 4. Boolean: Flag syntax (--flag) or explicit value (--flag=true) +# 5. Enum: Type-constrained values with validation against allowed set +# 6. Array: Multiple values of specified item type +# 7. Formatted: String with format validation (IP, email, etc.) +# +# Option Syntax Support: +# - Flag syntax: --option value +# - Assignment syntax: --option=value +# - Boolean flag syntax: --option (implies true) +# - Array multiple: --option value1 --option value2 +# - Array comma-separated: --option=value1,value2 +# +# Validation Rules: +# - Optional options can be omitted +# - Required options must be provided (rare for options) +# - Type validation occurs before action execution +# - Enum validation shows allowed values in error messages +# - Array validation applies to each item individually +# - Default values are validated against type and enum constraints +# +# Error Message Types: +# - JSON Schema validation errors for missing/invalid properties +# - Go strconv parsing errors for type conversion failures +# - Format-specific validation errors for formatted strings +# - Command-line parsing errors for syntax issues +# - Array-specific errors with item index identification +# +# Default Value Behavior: +# - Used when options are not provided +# - Must conform to type and enum constraints +# - Invalid defaults cause validation errors +# - Array defaults can be empty arrays or contain values +# - Boolean defaults work with flag syntax +# +# Array Option Behavior: +# - Support multiple --option calls to build array +# - Support comma-separated values in single call +# - Type validation applies to each array item +# - Enum validation applies to each array item +# - Boolean arrays require explicit values (no flag syntax) +# - Empty arrays are valid defaults +# +# Required Options: +# - Rare but supported for options +# - Must be provided even if they have default values +# - Generate command-line parsing errors when missing +# - Different error messages than JSON Schema validation +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/processors.txtar b/test/testdata/action/input/processors.txtar new file mode 100644 index 0000000..4c69908 --- /dev/null +++ b/test/testdata/action/input/processors.txtar @@ -0,0 +1 @@ +# Test processors \ No newline at end of file diff --git a/test/testdata/action/input/shorthand-duplicates.txtar b/test/testdata/action/input/shorthand-duplicates.txtar new file mode 100644 index 0000000..5be4363 --- /dev/null +++ b/test/testdata/action/input/shorthand-duplicates.txtar @@ -0,0 +1,159 @@ +# ============================================================================= +# Launchr Action Option Shorthand Conflict Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Handle duplicate shorthand definitions within a single action +# 2. Resolve shorthand conflicts with predictable behavior +# 3. Support mixed usage of shorthand and long-form options +# 4. Process multiple options with the same shorthand correctly +# 5. Maintain independent processing of different options +# 6. Apply shorthand mapping consistently across option combinations +# +# Test Focus: +# - Shorthand conflict resolution behavior +# - Priority handling when multiple options share shorthand +# - Mixed shorthand/long-form option processing +# - Option independence with conflicting shorthands +# - Predictable shorthand mapping behavior +# ============================================================================= + +# Test 1: Shorthand Usage with Conflict (First Option Priority) +# ----------------------------------------------------------------------------- +# Execute with shorthand that maps to multiple options +exec launchr test-print-input:shorthand -i 73 + +# Validate shorthand maps to first defined option: +# Should process the first option with matching shorthand +stdout '^optInteger: 73 int true$' + +# Validate second option remains unset: +# Should show second option as nil since shorthand didn't map to it +stdout '^optIntegerDup: false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Shorthand with Long-Form Override +# ----------------------------------------------------------------------------- +# Execute with shorthand and explicit long-form for same option +exec launchr test-print-input:shorthand -i 73 --optInteger=37 + +# Validate long-form takes precedence: +# Should use the long-form value (37) over shorthand value (73) +stdout '^optInteger: 37 int true$' + +# Validate second option remains unset: +# Should show second option as nil since it wasn't specified +stdout '^optIntegerDup: false$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: Shorthand with Different Long-Form Option +# ----------------------------------------------------------------------------- +# Execute with shorthand and long-form for different option +exec launchr test-print-input:shorthand -i 73 --optIntegerDup=37 + +# Validate shorthand maps to first option: +# Should process first option using shorthand value +stdout '^optInteger: 73 int true$' + +# Validate second option uses long-form: +# Should process second option using explicit long-form value +stdout '^optIntegerDup: 37 int true$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configuration with Duplicate Shorthand +# ============================================================================= + +# Shorthand Conflict Action +-- test-print-input/actions/shorthand/action.yaml -- +# Plugin action demonstrating shorthand conflict handling +# Shows behavior when multiple options define the same shorthand +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input - Option shorthand # Human-readable action name + + options: + # First option with shorthand 'i' + - name: optInteger # Option identifier + shorthand: i # Shorthand alias (will have priority) + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + + # Second option with same shorthand 'i' (potential conflict) + - name: optIntegerDup # Different option identifier + shorthand: i # Same shorthand alias (conflict) + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Shorthand Conflict Resolution: +# 1. First-defined option gets priority for shorthand mapping +# 2. Subsequent options with same shorthand are ignored for shorthand usage +# 3. Long-form option names always work regardless of shorthand conflicts +# 4. No error is generated for duplicate shorthand definitions +# 5. Behavior is predictable and deterministic based on definition order +# +# Priority Rules: +# - Declaration order determines shorthand priority +# - First option in options array gets the shorthand +# - Later options with same shorthand lose shorthand access +# - Long-form names remain unaffected by shorthand conflicts +# +# Option Independence: +# - Each option maintains its own value and state +# - Shorthand conflicts don't affect option functionality +# - Long-form access works for all options regardless of shorthand +# - Options can be set independently using long-form names +# +# Command-Line Processing: +# - Shorthand -i maps to first option with shorthand 'i' +# - Long-form --optInteger always maps to optInteger +# - Long-form --optIntegerDup always maps to optIntegerDup +# - Multiple flags can be used in same command +# +# Value Precedence: +# - Later flags override earlier flags for same option +# - Long-form and shorthand can target same option +# - Last specified value wins for any given option +# - Different options maintain independent values +# +# Error Handling: +# - No errors for duplicate shorthand definitions +# - No warnings about shorthand conflicts +# - Silent priority-based resolution +# - Normal validation applies to all options +# +# Design Implications: +# - Shorthand conflicts are handled gracefully +# - Predictable behavior based on simple rules +# - No complex conflict resolution needed +# - Maintains backward compatibility +# - Encourages unique shorthand usage in practice +# +# Best Practices: +# - Use unique shorthand characters per action +# - Document shorthand conflicts if intentional +# - Prefer long-form names for clarity +# - Test shorthand behavior when conflicts exist +# - Consider shorthand assignment carefully in action design +# +# Use Cases: +# - Graceful handling of configuration errors +# - Backward compatibility when adding new options +# - Simple conflict resolution without user intervention +# - Predictable behavior for automated tools +# - Clear precedence rules for option processing +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/shorthand-incorrect.txtar b/test/testdata/action/input/shorthand-incorrect.txtar new file mode 100644 index 0000000..1be948d --- /dev/null +++ b/test/testdata/action/input/shorthand-incorrect.txtar @@ -0,0 +1,119 @@ +# ============================================================================= +# Launchr Action Option Shorthand Validation Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Validate shorthand flag definitions for correct format +# 2. Handle invalid shorthand configurations gracefully +# 3. Provide clear error messages for shorthand parsing failures +# 4. Enforce shorthand format constraints (single character requirement) +# 5. Distinguish between shorthand definition errors and usage errors +# 6. Maintain consistent error handling for shorthand validation +# +# Test Focus: +# - Shorthand format validation (single character constraint) +# - Error handling for multi-character shorthand definitions +# - Command-line parsing behavior with invalid shorthand configs +# - Error message clarity for shorthand format violations +# - Separation of configuration errors from usage errors +# ============================================================================= + +# Test 1: Invalid Multi-Character Shorthand Usage +# ----------------------------------------------------------------------------- +# Execute with command that would use invalid multi-character shorthand +! exec launchr test-print-input:shorthand -int 73 + +# Validate shorthand parsing error: +# Should show command-line parsing error for invalid shorthand format +# The error indicates that 'i' is recognized as shorthand, not 'int' +stdout 'unknown shorthand flag: ''i'' in -int' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configuration with Invalid Shorthand +# ============================================================================= + +# Invalid Shorthand Action +-- test-print-input/actions/shorthand/action.yaml -- +# Plugin action demonstrating invalid shorthand configuration +# Shows behavior when shorthand violates single-character constraint +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input - Option shorthand # Human-readable action name + + options: + - name: optInteger # Option identifier + shorthand: int # Invalid: multi-character shorthand + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Shorthand Format Constraints: +# 1. Shorthand must be single character (not enforced at config level) +# 2. Multi-character shorthand definitions cause parsing ambiguity +# 3. Command-line parser treats multi-character shorthand as invalid +# 4. Only first character of multi-character shorthand is recognized +# 5. Error messages reflect command-line parsing perspective +# +# Error Behavior Analysis: +# - Configuration allows multi-character shorthand definition +# - Command-line parser only recognizes first character +# - Flag '-int' is parsed as shorthand 'i' + characters 'nt' +# - Parser reports 'i' as unknown shorthand, not 'int' +# - This reveals internal parsing behavior and constraints +# +# Parsing Logic: +# - Short flags expect single character after dash +# - Multi-character sequences after dash are parsed character by character +# - First character is treated as potential shorthand +# - Remaining characters are treated as additional flag parsing +# - Unknown shorthand generates specific error message +# +# Configuration vs Runtime Validation: +# - Configuration schema doesn't enforce single-character shorthand +# - Runtime parsing enforces single-character shorthand behavior +# - Mismatch between configuration flexibility and parser constraints +# - Error occurs at usage time, not configuration time +# +# Error Message Interpretation: +# - "unknown shorthand flag: 'i' in -int" means: +# - Parser found 'i' as first character of '-int' +# - 'i' is not a defined shorthand for this action +# - Full string '-int' was provided but parsed as '-i' + 'nt' +# - This is a parsing error, not a shorthand definition error +# +# Design Implications: +# - Shorthand validation should occur at configuration time +# - Error messages could be more specific about format requirements +# - Configuration schema should enforce single-character constraint +# - Runtime errors should distinguish format violations from unknown shortcuts +# +# Best Practices: +# - Always use single-character shorthand definitions +# - Validate shorthand format during action configuration +# - Use descriptive error messages for shorthand violations +# - Consider configuration-time validation for shorthand constraints +# - Document shorthand format requirements clearly +# +# Potential Improvements: +# - Add configuration-time validation for shorthand format +# - Improve error messages to explain shorthand format requirements +# - Consider rejecting multi-character shorthand at config load time +# - Provide clearer distinction between config and usage errors +# - Add shorthand format documentation to action schema +# +# Technical Details: +# - Command-line parsing treats '-int' as '-i' + 'nt' +# - Only 'i' is checked against defined shorthand mappings +# - Multi-character shorthand definitions are effectively ignored +# - Parser behavior is consistent with Unix flag parsing conventions +# - Single-character shorthand is a fundamental constraint of the parsing model +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/shorthand.txtar b/test/testdata/action/input/shorthand.txtar new file mode 100644 index 0000000..4848c02 --- /dev/null +++ b/test/testdata/action/input/shorthand.txtar @@ -0,0 +1,171 @@ +# ============================================================================= +# Launchr Action Option Shorthand Test Suite +# ============================================================================= +# +# This test file validates the Launchr tool's ability to: +# 1. Support shorthand flags for options (single character alternatives) +# 2. Handle both long-form (--option) and short-form (-o) option syntax +# 3. Validate shorthand flag recognition and processing +# 4. Provide appropriate error messages for unknown shorthand flags +# 5. Support shorthand flags across multiple actions +# 6. Maintain consistent behavior between long and short option forms +# +# Test Focus: +# - Shorthand flag definition and usage +# - Command-line parsing for short flags +# - Error handling for undefined shorthand flags +# - Equivalence between long and short option forms +# - Shorthand flag reusability across actions +# ============================================================================= + +# Test 1: Long-Form Option Syntax +# ----------------------------------------------------------------------------- +# Execute with standard long-form option syntax +exec launchr test-print-input:shorthand --optInteger 73 + +# Validate successful long-form option processing: +# Should process the integer option normally using full name +stdout '^optInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Short-Form Option Syntax (Valid Shorthand) +# ----------------------------------------------------------------------------- +# Execute with short-form option syntax using defined shorthand +exec launchr test-print-input:shorthand -i 73 + +# Validate successful shorthand option processing: +# Should process the integer option identically to long-form +stdout '^optInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: Invalid Shorthand Flag +# ----------------------------------------------------------------------------- +# Execute with undefined shorthand flag +! exec launchr test-print-input:shorthand -n 73 + +# Validate unknown shorthand error message: +# Should show command-line parsing error for unknown shorthand +stdout 'unknown shorthand flag: ''n'' in -n' + +# Validate option was not processed: +# Should not show option processing output due to error +! stdout '^optInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# Test 4: Shorthand Reusability Across Actions +# ----------------------------------------------------------------------------- +# Execute different action with same shorthand flag +exec launchr test-print-input:shorthand2 -i 73 + +# Validate shorthand works across different actions: +# Should process the integer option using shorthand in different action +stdout '^optInteger: 73 int true$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Test Data Files - Action Configurations with Shorthand Options +# ============================================================================= + +# First Shorthand Action +-- test-print-input/actions/shorthand/action.yaml -- +# Plugin action demonstrating option shorthand functionality +# Shows how to define single-character aliases for options +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input - Option shorthand # Human-readable action name + + options: + - name: optInteger # Option identifier + shorthand: i # Single-character shorthand alias + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + +# Second Shorthand Action (Same Shorthand) +-- test-print-input/actions/shorthand2/action.yaml -- +# Plugin action demonstrating shorthand reusability across actions +# Shows that same shorthand can be used in different actions +runtime: plugin # Plugin execution type + +action: + title: Test Plugin - Input - Option shorthand # Human-readable action name + + options: + - name: optInteger # Option identifier + shorthand: i # Same single-character shorthand alias + title: Option Integer # Human-readable option name + description: This is an optional integer option # Help text + type: integer # Explicit integer type + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Shorthand Flag Support: +# 1. Single-character aliases for options using 'shorthand' property +# 2. Short flags use single dash syntax: -i instead of --optInteger +# 3. Functional equivalence between long and short forms +# 4. Same validation and processing for both forms +# 5. Error handling for undefined shorthand flags +# +# Command-Line Syntax: +# - Long form: --optionName value +# - Short form: -s value (where 's' is the shorthand) +# - Assignment form: --optionName=value or -s=value +# - Boolean flags: --flag or -f (for boolean options) +# +# Shorthand Definition Rules: +# - Must be single character +# - Defined per option in action configuration +# - Case-sensitive (presumably) +# - Optional feature (not all options need shorthand) +# +# Error Handling: +# - Unknown shorthand flags generate parsing errors +# - Error messages identify the specific unknown flag +# - Processing stops on shorthand errors +# - Clear distinction between shorthand and option name errors +# +# Scope and Reusability: +# - Shorthand flags are scoped to individual actions +# - Same shorthand can be reused across different actions +# - No global shorthand conflict resolution needed +# - Each action defines its own shorthand namespace +# +# Processing Equivalence: +# - Short and long forms produce identical output +# - Same type validation and conversion +# - Same error handling for invalid values +# - Same default value behavior +# - Same required option enforcement +# +# Integration with Other Features: +# - Works with all option types (integer, string, boolean, array, etc.) +# - Compatible with default values +# - Compatible with required options +# - Compatible with enum validation +# - Compatible with format validation +# +# User Experience Benefits: +# - Faster typing for frequently used options +# - Reduced command line length +# - Familiar Unix-style short flag conventions +# - Maintains full option name availability +# - Clear error messages when shortcuts are misused +# +# Implementation Considerations: +# - Command-line parsing library handles short flag recognition +# - Shorthand mapping occurs at action definition level +# - No runtime shorthand registration or conflicts +# - Each action maintains independent shorthand namespace +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/action/input/template.txtar b/test/testdata/action/input/template.txtar new file mode 100644 index 0000000..6346fb4 --- /dev/null +++ b/test/testdata/action/input/template.txtar @@ -0,0 +1 @@ +# TODO Test template values: args and options \ No newline at end of file diff --git a/test/testdata/build/build.txtar b/test/testdata/build/build.txtar new file mode 100644 index 0000000..ae6148e --- /dev/null +++ b/test/testdata/build/build.txtar @@ -0,0 +1,128 @@ +# ============================================================================= +# Launchr Binary Building and Version Verification Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Build binaries with proper version ldflags injection +# 2. Perform recursive binary building (building new binaries with existing ones) +# 3. Validate application names according to naming conventions +# 4. Handle plugin integration and version reporting +# 5. Apply core package replacement during build process +# +# Test Structure: +# - Tests basic binary building with version information +# - Tests recursive building with core replacement tracking +# - Tests input validation for application naming rules +# - Validates version output formatting and content matching +# ============================================================================= + +# Setup Phase: Environment Configuration +# ----------------------------------------------------------------------------- +# Configure application metadata for testing +# These variables define the test application identity and version information +env APP_NAME=testapp +env APP_VERSION='v0.0.0-testscript' +env APP_BUILT_WITH='testscript v0.0.0' + +# Define validation patterns for architecture strings +# This regex validates the expected architecture format in version output +env ARCH_RGX=[a-z0-9]+/[a-z0-9]+ + +# Compute expected version strings for validation +# This creates the expected short version format for comparison +env APP_VERSION_SHORT=$APP_NAME' version '${APP_VERSION@R}' '$ARCH_RGX + +# Configure build environment with real home for caching +# Reuse actual home directory to enable build caching optimization +env HOME=$REAL_HOME + +# Test 1: Basic Binary Build and Version Verification +# ----------------------------------------------------------------------------- +# Execute version command to verify basic build functionality +exec testapp --version + +# Validate version output format: +# Expected format: "testapp version X.Y.Z arch\nBuilt with ..." +stdout ^$APP_VERSION_SHORT'\nBuilt with '${APP_BUILT_WITH@R}\z$ + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Recursive Binary Building with Core Replacement +# ----------------------------------------------------------------------------- +# Configure second binary parameters for recursive build testing +# These variables define the new binary that will be built using the first one +env APP_NAME_2=${APP_NAME}new +env APP_VERSION_2='v1.2.0-testscript' + +# Define expected version strings for core replacement validation +# These patterns validate that core replacement information is properly tracked +env APP_VERSION_CORE='Core version: v.*\nCore replace: '${CORE_PKG@R}' v.* => '${REPO_PATH@R}' \(devel\)' +env APP_VERSION_FULL=$APP_NAME_2' version '$APP_VERSION_2' '$ARCH_RGX'\nBuilt with '${APP_VERSION_SHORT}'\n'$APP_VERSION_CORE + +# Verify target binary does not exist before build +! exists $APP_NAME_2 + +# Execute recursive build with core replacement +# This command builds a new binary using the previously built binary +exec testapp build -n $APP_NAME_2 -o $APP_NAME_2 -r $CORE_PKG=$REPO_PATH --build-version $APP_VERSION_2 + +# Validate clean build execution (no error output) +! stderr . + +# Verify target binary was successfully created +exists $APP_NAME_2 + +# Execute version command on newly built binary +exec ./$APP_NAME_2 --version + +# Validate full version output includes core replacement information +# This ensures the recursive build properly tracked version lineage +stdout ^$APP_VERSION_FULL'\z$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: Input Validation - Invalid Application Name +# ----------------------------------------------------------------------------- +# Verify target binary does not exist before invalid build attempt +! exists under_score + +# Attempt to build with invalid name containing underscore +# This should fail due to naming convention violations +! exec testapp build --name under_score --output under_score --build-version invalid + +# Validate appropriate error message for invalid name +# The system should clearly indicate why the name is invalid +stdout 'invalid application name "under_score"' + +# Validate clean error handling (no stderr output) +! stderr . + +# Verify no binary was created due to validation failure +! exists under_score + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Binary Building Rules: +# 1. Accept valid application names (letters, numbers, hyphens) +# 2. Reject invalid names (underscores, special characters) +# 3. Inject version information via ldflags during build +# 4. Track build lineage in recursive building scenarios +# 5. Properly handle core package replacement +# +# Version Output Format: +# - Basic: "app version X.Y.Z arch" +# - Extended: Includes "Built with" information +# - Recursive: Includes core replacement details +# - Clean error handling with descriptive messages +# +# Naming Conventions: +# - Valid characters: letters, numbers, hyphens +# - Invalid characters: underscores, special symbols +# - Case sensitivity applies +# - Length restrictions may apply +# +# ============================================================================= diff --git a/test/testdata/build/debug.txtar b/test/testdata/build/debug.txtar new file mode 100644 index 0000000..ffff5aa --- /dev/null +++ b/test/testdata/build/debug.txtar @@ -0,0 +1,198 @@ +# ============================================================================= +# Launchr Debug Build Directory Preservation Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Execute regular builds without creating persistent debug directories +# 2. Execute debug builds with temporary directory preservation +# 3. Generate all required build artifacts in debug mode +# 4. Maintain proper directory structure during debug builds +# 5. Preserve build directories for inspection and debugging +# 6. Ensure proper cleanup behavior (only one build directory exists) +# 7. Create complete Go module structure with dependencies +# 8. Validate debug header inclusion in built binaries +# 9. Verify non-debug builds exclude debug headers +# +# Test Structure: +# - Tests initial build without debug flag (baseline verification) +# - Tests debug flag functionality and directory preservation +# - Tests build artifact generation and completeness +# - Validates expected file presence and structure +# - Confirms proper temporary directory management and cleanup +# - Verifies that only one build directory exists after debug build +# - Tests debug header presence/absence in binary version output +# ============================================================================= + +# Setup Phase: Environment Configuration +# ----------------------------------------------------------------------------- +# Configure build environment with real home for caching optimization +# Reuse actual home directory to enable build caching and dependency resolution +env HOME=$REAL_HOME +env GOTMPDIR=$TMPDIR + +# Test 1.1: Pre-Debug Build Execution (Build Directory Cleanup Verification) +# ----------------------------------------------------------------------------- +# Execute initial build without debug flag to establish baseline state +# This ensures that non-debug builds don't create persistent build directories +# and prepares the environment for testing debug directory preservation +# +# Purpose: This step is critical for validating that subsequent debug builds +# properly manage temporary directories and maintain exactly one build directory +# when debug mode is enabled. By running a non-debug build first, we ensure +# that the debug build test starts from a clean state. +exec launchr build -r $CORE_PKG=$REPO_PATH + +# Validate non-debug build behavior: no debug output should be present +# Non-debug builds should not display debug flag messages or directory paths +# This confirms that debug output is only shown when --debug flag is used +! stdout 'Debug flag is set.*: .*/launchr/build_\d+' + +# Validate clean execution (no error output) +! stderr . + +# Execute version check on non-debug built binary +# Non-debug builds should NOT include debug headers in version output +# This establishes baseline behavior before testing debug build functionality +exec ./launchr --version +! stdout '^Built with debug headers$' + +# Test 2: Debug Build Execution and Directory Preservation +# ----------------------------------------------------------------------------- +# Execute debug build with core package replacement +# The --debug flag should preserve temporary build directories for inspection +# This test validates that debug mode creates exactly one persistent build directory +# +# Critical behavior: When debug mode is enabled, the build system should: +# 1. Create a uniquely named temporary directory under $TMPDIR/launchr/ +# 2. Preserve this directory after build completion (not cleanup) +# 3. Display debug information including the directory path +# 4. Ensure only one build directory exists (cleanup previous debug builds) +# 5. Include debug headers in the compiled binary +exec launchr build --debug -r $CORE_PKG=$REPO_PATH + +# Validate debug mode activation and directory preservation +# Expected output should indicate debug flag is set and show temp directory path +# Format: "Debug flag is set: /path/to/launchr/build_" +stdout 'Debug flag is set.*: .*/launchr/build_\d+' + +# Validate clean execution (no error output) +! stderr . + +# Execute version check on debug-built binary +# Debug builds MUST include debug headers in version output +# This validates that debug mode properly embeds debug information in binary +exec ./launchr --version +stdout '^Built with debug headers$' + +# Test 2: Build Artifact Validation and Directory Uniqueness +# ----------------------------------------------------------------------------- +# Execute validation script to verify build directory contents +# This script checks for presence of all expected build artifacts +# AND validates that exactly one build directory exists (testing cleanup behavior) +# +# The validation script serves dual purposes: +# 1. Verify all required build artifacts are generated correctly +# 2. Confirm that debug build cleanup maintains exactly one build directory +# This ensures that repeated debug builds don't accumulate temporary directories +exec sh check_build_dir.sh + +# Validate that all expected files are present in build directory +# The script should confirm successful generation of all build artifacts +stdout '^All expected files are present$' + +# ============================================================================= +# Build Directory Validation Script +# ============================================================================= +# This script validates the contents of the preserved debug build directory +# and ensures all necessary build artifacts are properly generated +-- check_build_dir.sh -- +#!/bin/sh +set -eu + +# Define base directory path for build artifacts +# All temporary build directories are created under this path +BASE_DIR="$TMPDIR/launchr" + +# Verify base directory exists +# The base directory should be created during debug build execution +if [ ! -d "$BASE_DIR" ]; then + echo "Error: Directory $BASE_DIR does not exist." + exit 1 +fi + +# Locate and validate build directory +# Debug builds create uniquely named build_* directories with random number +build_dir_count=0 +build_dir="" +for dir in "$BASE_DIR"/build_*; do + if [ -d "$dir" ]; then + build_dir_count=$((build_dir_count + 1)) + build_dir="$dir" + fi +done + +# Validate exactly one build directory exists +# Multiple directories indicate cleanup issues or concurrent builds +# This test ensures that successive debug builds properly clean up previous directories +# and maintain exactly one active build directory for inspection +if [ "$build_dir_count" -eq 0 ]; then + echo "Error: No build_* directory found in $BASE_DIR" + exit 1 +elif [ "$build_dir_count" -gt 1 ]; then + echo "Error: Multiple build_* directories found" + echo "This indicates debug build cleanup is not working properly" + exit 1 +fi + +# Report found build directory and change to it +echo "Found build directory: $build_dir" +cd "$build_dir" || exit 1 + +# Validate presence of essential build artifacts +# These files are required for successful Go module compilation +for file in "main.go" "gen.go" "plugins.go" "go.mod" "go.sum"; do + if [ ! -e "$file" ]; then + echo "Missing expected file: $file" + exit 1 + else + echo "Found expected file: $file" + fi +done + +# Report successful validation +echo "All expected files are present" + +# ============================================================================= +# Expected Build Artifacts and Debug Header Validation +# ============================================================================= +# +# Required Files: +# - main.go: Primary application entry point +# - gen.go: Generated code for action discovery and integration +# - plugins.go: Plugin registration and initialization code +# - go.mod: Go module definition with dependencies +# - go.sum: Dependency checksums for reproducible builds +# +# Directory Structure: +# - Base: $TMPDIR/launchr/ +# - Build: $TMPDIR/launchr/build_/ +# - Artifacts: All files directly in build directory +# +# Validation Rules: +# - Exactly one build_* directory should exist +# - All required files must be present +# - Files should be accessible and non-empty +# - Directory structure should be clean and organized +# +# Debug Build Behavior: +# - Non-debug builds: No persistent directories created, no debug headers in binary +# - Debug builds: Single persistent directory preserved, debug headers included in binary +# - Cleanup: Previous debug directories removed automatically +# - Uniqueness: Each debug build creates uniquely named directory +# +# Binary Version Output: +# - Non-debug builds: Standard version output without debug headers +# - Debug builds: Version output includes "Built with debug headers" message +# - This distinction allows verification of debug mode compilation success +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/build/no-cache.txtar b/test/testdata/build/no-cache.txtar new file mode 100644 index 0000000..153a053 --- /dev/null +++ b/test/testdata/build/no-cache.txtar @@ -0,0 +1,87 @@ +# ============================================================================= +# Launchr Cache Behavior and Go Module Resolution Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Utilize Go proxy caching for efficient module resolution +# 2. Handle --no-cache flag for direct source downloading +# 3. Resolve pseudo-versions with Go module version magic +# 4. Manage plugin dependencies from submodules correctly +# 5. Provide appropriate error handling for unsupported scenarios +# +# Test Structure: +# - Tests standard caching behavior with Go proxy resolution +# - Tests --no-cache flag with pseudo-version specification +# - Tests --no-cache limitations with submodule dependencies +# - Validates error messages and build output formatting +# ============================================================================= + +# Setup Phase: Environment Configuration +# ----------------------------------------------------------------------------- +# Configure build environment with real home for caching optimization +# Reuse actual home directory to enable Go module caching and proxy access +env HOME=$REAL_HOME + +# Define plugin repository for testing module resolution +# This repository contains submodules that test various resolution scenarios +env PLUGIN_REPO=golang.org/x/example/hello + +# Test 1: Standard Build with Go Proxy Caching +# ----------------------------------------------------------------------------- +# Execute build with verbose output and plugin dependency +# This should succeed using Go proxy magic and module caching +exec launchr build -v -r $CORE_PKG=$REPO_PATH -p ${PLUGIN_REPO}/reverse + +# Validate clean execution (no error output) +# Go proxy should resolve dependencies without errors +! stderr . + +# Test 2: No-Cache Build with Pseudo-Version Resolution +# ----------------------------------------------------------------------------- +# Execute build with --no-cache flag and master branch pseudo-version +# This tests direct source downloading with Go module version magic +exec launchr build --no-cache -v -r $CORE_PKG=$REPO_PATH -p ${PLUGIN_REPO}/reverse@master + +# Validate Go module addition with pseudo-version format +# Expected format: "go: added v0.0.0--" +stdout '^go: added '${PLUGIN_REPO@R}' v0\.0\.0-\d+-.+$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: No-Cache Build Limitation with Submodules +# ----------------------------------------------------------------------------- +# Attempt build with --no-cache flag without version specification +# This should fail because submodules cannot be resolved without Go proxy magic +! exec launchr build -v --no-cache -r $CORE_PKG=$REPO_PATH -p ${PLUGIN_REPO}/reverse + +# Validate expected error message for submodule resolution failure +# Go should report no matching versions for upgrade query on submodules +stdout '^go: '${PLUGIN_REPO}'/reverse: no matching versions for query "upgrade"$' + +# Validate clean error handling (no stderr output) +! stderr . + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# No-Cache Behavior: +# 1. --no-cache forces direct source downloading +# 2. Pseudo-versions (e.g., @master) enable direct resolution +# 3. Submodules without versions cannot be resolved directly +# 4. Error messages clearly indicate resolution failures +# +# Module Resolution Rules: +# - With cache: Go proxy resolves all dependencies automatically +# - Without cache + version: Direct source download succeeds +# - Without cache + no version: Submodule resolution fails +# - Pseudo-versions follow format: v0.0.0-- +# +# Error Handling: +# - Clear error messages for unsupported scenarios +# - No stderr output for expected failures +# - Proper exit codes for different failure types +# - Verbose output shows Go module operations +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/build/plugins.txtar b/test/testdata/build/plugins.txtar new file mode 100644 index 0000000..a59e21d --- /dev/null +++ b/test/testdata/build/plugins.txtar @@ -0,0 +1,125 @@ +# ============================================================================= +# Launchr Plugin Integration and Generate Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Build binaries with external plugin dependencies from public repositories +# 2. Integrate local plugins with version replacement and path mapping +# 3. Generate proper version output including plugin information +# 4. Create functional actions from plugin-generated code +# 5. Handle mixed plugin scenarios (external + local) correctly +# +# Test Structure: +# - Tests plugin integration during binary building +# - Tests version reporting with plugin information +# - Tests generate functionality +# - Validates plugin replacement and version tracking +# ============================================================================= + +# Setup Phase: Environment Configuration +# ----------------------------------------------------------------------------- +# Configure application metadata for testing +# These variables define the test application identity and version information +env APP_NAME=launchr +env APP_VERSION='v0.0.0-testscript' +env APP_BUILT_WITH='testscript v0.0.0' + +# Define validation patterns for architecture strings +# This regex validates the expected architecture format in version output +env ARCH_RGX=[a-z0-9]+/[a-z0-9]+ + +# Compute expected version strings for validation +# This creates the expected short version format for comparison +env APP_VERSION_SHORT=$APP_NAME' version '${APP_VERSION@R}' '$ARCH_RGX + +# Configure build environment with real home for caching optimization +# Reuse actual home directory to enable build caching and dependency resolution +env HOME=$REAL_HOME + +# Define expected version strings for core replacement validation +# These patterns validate that core replacement information is properly tracked +env APP_VERSION_CORE='Core version: v.*\nCore replace: '${CORE_PKG@R}' v.* => '${REPO_PATH@R}' \(devel\)' +env APP_VERSION_FULL='launchr version dev '$ARCH_RGX'\nBuilt with '${APP_VERSION_SHORT}'\n'$APP_VERSION_CORE + +# Plugin Configuration +# ----------------------------------------------------------------------------- +# Configure an external plugin from a public repository. +# This plugin is intentionally not a Launchr plugin, but it is acceptable for integration testing. +# We include it to verify that public URLs are supported. +# We avoid using a real plugin, as it may not be compatible with the current version. +env APP_PLUGIN_EXTERNAL=golang.org/x/example/hello/reverse@master + +# Configure local plugin with version and replacement path +# This tests local plugin integration with path mapping functionality +env APP_PLUGIN_LOCAL=example.com/genaction@v1.1.1 +env APP_PLUGIN_LOCAL_PATH=$REPO_PATH/test/plugins/genaction + +# Test 1: Plugin Integration During Binary Building +# ----------------------------------------------------------------------------- +# Verify target binary does not exist before build +! exists $APP_NAME + +# Execute build with mixed plugin configuration +# This command integrates both external and local plugins with core replacement +exec launchr build -r $CORE_PKG=$REPO_PATH -p $APP_PLUGIN_EXTERNAL -p $APP_PLUGIN_LOCAL -r $APP_PLUGIN_LOCAL=$APP_PLUGIN_LOCAL_PATH + +# Validate clean build execution (no error output) +! stderr . + +# Verify target binary was successfully created +exists $APP_NAME + +# Test 2: Version Reporting with Plugin Information +# ----------------------------------------------------------------------------- +# Execute version command on plugin-integrated binary +exec ./$APP_NAME --version + +# Validate version output includes complete plugin information +# Expected format includes core version, plugin list, and replacement details +stdout ^$APP_VERSION_FULL'\nPlugins:\n - example\.com/genaction v1\.1\.1\n - example\.com/genaction v1\.1\.1 => '$REPO_PATH'/test/plugins/genaction \(devel\)\n\z$' + +# Validate clean execution (no error output) +! stderr . + +# Test 3: Generated Action Functionality +# ----------------------------------------------------------------------------- +# Execute the generated action from the integrated plugin +# This tests that plugin-generated actions are properly functional +exec ./$APP_NAME genaction:example + +# Validate the action produces expected output +# The plugin should generate a working action that outputs the expected message +stdout 'hello world' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Plugin Integration Rules: +# 1. External plugins are resolved from public repositories +# 2. Local plugins use path replacement for development workflow +# 3. Both plugin types can be integrated in the same build +# 4. Plugin dependencies are properly resolved and linked +# 5. Version information tracks all plugin sources and replacements +# +# Version Output Format: +# - Core version with replacement information +# - Plugin list with version numbers +# - Replacement paths for local plugins marked as (devel) +# - Clean formatting with proper line breaks and indentation +# +# Generate Behavior: +# - Generation is done during build process +# - Plugin-generated actions become available as commands +# - Actions execute with expected functionality +# - Clean output without errors or warnings +# +# Build Process: +# - Validates plugin compatibility before integration +# - Maintains proper dependency resolution +# - Creates fully functional binary with plugins +# +# ============================================================================= diff --git a/test/testdata/build/tags.txtar b/test/testdata/build/tags.txtar new file mode 100644 index 0000000..51ccfda --- /dev/null +++ b/test/testdata/build/tags.txtar @@ -0,0 +1,88 @@ +# ============================================================================= +# Launchr Build Tags and Plugin Compilation Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Apply custom build tags during binary compilation +# 2. Generate plugin actions that respect build tag configurations +# 3. Integrate local plugins with custom tag-aware builds +# +# Test Structure: +# - Tests custom build tag application during compilation +# - Tests plugin integration with tag-aware building +# - Tests generate functionality with tag-specific behavior +# - Validates tag propagation through the entire build pipeline +# ============================================================================= + +# Setup Phase: Environment Configuration +# ----------------------------------------------------------------------------- +# Configure build environment with real home for caching optimization +# Reuse actual home directory to enable build caching and dependency resolution +env HOME=$REAL_HOME + +# Define application name for build target +# This specifies the binary name that will be created during the build process +env APP_NAME=launchr + +# Plugin Configuration +# ----------------------------------------------------------------------------- +# Configure local plugin with version and replacement path +# This tests local plugin integration with custom build tag functionality +env APP_PLUGIN_LOCAL=example.com/genaction@v1.1.1 +env APP_PLUGIN_LOCAL_PATH=$REPO_PATH/test/plugins/genaction + +# Test 1: Custom Build Tag Application and Plugin Integration +# ----------------------------------------------------------------------------- +# Execute build with custom tag and local plugin integration +# The --tag flag should propagate to plugin compilation and affect behavior +exec launchr build --tag customtag -r $CORE_PKG=$REPO_PATH -p $APP_PLUGIN_LOCAL -r $APP_PLUGIN_LOCAL=$APP_PLUGIN_LOCAL_PATH + +# Validate clean build execution (no error output) +! stderr . + +# Verify target binary was successfully created +exists ./launchr + +# Test 2: Tag-Aware Generated Action Functionality +# ----------------------------------------------------------------------------- +# Execute the generated action from the tag-aware plugin +# This tests that build tags properly influence plugin action behavior +exec ./launchr genaction:example + +# Validate the action produces tag-specific output +# The custom tag should modify the action's output to include tag information +stdout '^hello world built with custom tag$' + +# Validate clean execution (no error output) +! stderr . + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Build Tag Behavior: +# 1. Custom tags are applied during binary compilation +# 2. Build tags propagate to plugin compilation process +# 3. Plugin code can conditionally respond to build tags +# 4. Tag-specific behavior is preserved in generated actions +# 5. Clean build process with tag integration +# +# Plugin Integration with Tags: +# - Local plugins respect build tag configurations +# - Plugin compilation includes tag-specific conditional code +# - Generated actions reflect tag-aware behavior +# - Tag information is properly passed through build pipeline +# +# Action Output Validation: +# - Tag-specific output confirms proper tag propagation +# - Expected format: base output + tag-specific modification +# - Clean execution without errors or warnings +# - Deterministic behavior based on tag configuration +# +# Build Process: +# - Validates tag compatibility with plugin system +# - Handles tag propagation to all compilation units +# - Maintains proper dependency resolution with tags +# - Creates fully functional binary with tag-aware actions +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/build/timeout.txtar b/test/testdata/build/timeout.txtar new file mode 100644 index 0000000..f306563 --- /dev/null +++ b/test/testdata/build/timeout.txtar @@ -0,0 +1,82 @@ +# ============================================================================= +# Launchr Build Timeout and Process Management Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Apply custom timeout limits during build operations +# 2. Terminate build processes when timeout threshold is exceeded +# 3. Provide clear timeout error messages to users +# 4. Handle timeout scenarios gracefully without system corruption +# 5. Validate timeout parameter parsing and enforcement +# +# Test Structure: +# - Tests timeout parameter application during build process +# - Tests build termination when timeout is exceeded +# - Tests error message formatting for timeout scenarios +# - Validates clean process termination and error handling +# ============================================================================= + +# Setup Phase: Environment Configuration +# ----------------------------------------------------------------------------- +# Configure build environment with real home for caching optimization +# Reuse actual home directory to enable build caching and dependency resolution +env HOME=$REAL_HOME + +# Test 1: Build Timeout Enforcement and Error Handling +# ----------------------------------------------------------------------------- +# Execute build with extremely short timeout to trigger timeout condition +# The 1-second timeout is intentionally too short for any meaningful build +! exec launchr build --timeout 1s + +# Validate timeout error message is displayed +# The system should clearly indicate that the build timed out after the specified duration +stdout 'build timed out after 1s' + +# Validate clean error handling (no stderr output) +# Timeout should be handled gracefully without system error messages +! stderr . + +# Test 2: Invalid Timeout Parameter Validation +# ----------------------------------------------------------------------------- +# Execute build with invalid timeout format to test parameter validation +# The "foo" value is not a valid Go duration format and should be rejected +! exec launchr build --timeout foo + +# Validate invalid duration error message is displayed +# The system should clearly indicate that the duration format is invalid +stdout 'time: invalid duration "foo"' + +# Validate clean error handling (no stderr output) +# Parameter validation errors should be handled gracefully +! stderr . + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Timeout Behavior: +# 1. Custom timeout values are parsed and applied correctly +# 2. Build process is terminated when timeout threshold is exceeded +# 3. Clear error messages indicate timeout condition and duration +# 4. Process termination is clean without system corruption +# 5. No binary artifacts are created when timeout occurs +# +# Error Handling: +# - Timeout errors are reported to stdout, not stderr +# - Error messages include specific timeout duration +# - Clean process termination without resource leaks +# - Proper exit codes for timeout conditions +# +# Timeout Parameter Format: +# - Supports standard Go duration format (e.g., 1s, 30m, 2h) +# - Validates timeout parameter syntax +# - Applies timeout to entire build process +# - Enforces timeout strictly without grace period +# +# Process Management: +# - Terminates build subprocess when timeout is reached +# - Cleans up temporary files and resources +# - Prevents incomplete builds from producing artifacts +# - Maintains system stability during forced termination +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/common/custom_app_name.txtar b/test/testdata/common/custom_app_name.txtar new file mode 100644 index 0000000..8468408 --- /dev/null +++ b/test/testdata/common/custom_app_name.txtar @@ -0,0 +1,111 @@ +# ============================================================================= +# Test Suite: Custom App Name Configuration (testapp) +# ============================================================================= +# +# This test validates that the custom app name 'testapp' correctly transforms +# environment variable names from the default LAUNCHR_* prefix to TESTAPP_* +# prefix while maintaining all functionality. +# +# Environment Variables Tested: +# - TESTAPP_LOG_LEVEL: Controls log level (should work like LAUNCHR_LOG_LEVEL) +# - TESTAPP_LOG_FORMAT: Controls log format (should work like LAUNCHR_LOG_FORMAT) +# - TESTAPP_QUIET_MODE: Enables quiet mode (should work like LAUNCHR_QUIET_MODE) +# - TESTAPP_ACTIONS_PATH: Custom action path (should work like LAUNCHR_ACTIONS_PATH) +# +# ============================================================================= + +# ============================================================================= +# Section 1: Test custom log format environment variable (TESTAPP_LOG_FORMAT) +# ============================================================================= + +# Test JSON format with custom app name +env TESTAPP_LOG_FORMAT=json +exec testapp --log-level=INFO testplugin:log-levels +stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . + +# ============================================================================= +# Section 2: Test custom log level environment variable (TESTAPP_LOG_LEVEL) +# ============================================================================= + +env TESTAPP_LOG_FORMAT=json + +# Test DEBUG level with custom app name +env TESTAPP_LOG_LEVEL=DEBUG +exec testapp testplugin:log-levels +stdout '^\{.*"level":"DEBUG".*\}$' +stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . + +# ============================================================================= +# Section 3: Test custom quiet mode environment variable (TESTAPP_QUIET_MODE) +# ============================================================================= + +# Test quiet mode with custom app name - should suppress all output +env TESTAPP_QUIET_MODE=1 +env TESTAPP_LOG_LEVEL=DEBUG +exec testapp testplugin:log-levels +! stdout . +! stderr . + +# Test that TESTAPP_QUIET_MODE=0 allows output +env TESTAPP_QUIET_MODE=0 +env TESTAPP_LOG_LEVEL=ERROR +exec testapp testplugin:log-levels +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . + +# ============================================================================= +# Section 4: Test custom actions path environment variable (TESTAPP_ACTIONS_PATH) +# ============================================================================= + +# Test that TESTAPP_ACTIONS_PATH correctly overrides action discovery path +env TESTAPP_ACTIONS_PATH=./foo +exec testapp --help +stdout '^\s+bar\.baz\.bar-bar:waldo-fred-thud\s+foo$' +! stderr . + +# Reset environment +env TESTAPP_ACTIONS_PATH= +exec testapp --help +stdout '^\s+foo\.baz\.bar-bar:waldo-fred-thud\s+foo$' +! stderr . + +# ============================================================================= +# Section 5: Verify that default LAUNCHR_* variables don't affect testapp +# ============================================================================= + +# Set LAUNCHR_* variables and verify they don't affect testapp behavior +env LAUNCHR_LOG_FORMAT=json +env LAUNCHR_LOG_LEVEL=DEBUG +env LAUNCHR_QUIET_MODE=1 + +# testapp should use its own defaults and ignore LAUNCHR_* variables +# Based on the output, testapp defaults to JSON format +exec testapp --log-level=INFO testplugin:log-levels +# Should produce JSON output (testapp's default) and not be quiet +stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . + +# ============================================================================= +# Test Data Files (reuse from existing tests) +# ============================================================================= + +-- foo/bar/baz/bar/bar_bar/actions/waldo-fred_thud/action.yaml -- +action: + title: foo +runtime: plugin + +-- .testapp/config.yaml -- +launchrctl: + actions_naming: + - search: ".bar." + replace: "." + - search: "_" + replace: "-" \ No newline at end of file diff --git a/test/testdata/common/logger.txtar b/test/testdata/common/logger.txtar new file mode 100644 index 0000000..7bb57ee --- /dev/null +++ b/test/testdata/common/logger.txtar @@ -0,0 +1,451 @@ +# ============================================================================= +# Test Suite: Verbosity configuration +# ============================================================================= +# +# Comprehensive test suite for Launchr's logging system, covering: +# - Log level configuration (DEBUG, INFO, WARN, ERROR, NONE) +# - Log format configuration (JSON, plain, pretty) +# - Verbosity flags (-v, -vv, -vvv, -vvv) +# - Environment variable configuration +# - Quiet mode functionality +# - Flag precedence and override behavior +# - Cross-format consistency validation +# +# This test suite ensures that all logging configuration methods produce +# consistent and expected output across different scenarios. +# +# Environment Variables Tested: +# - LAUNCHR_LOG_LEVEL: Controls log level (DEBUG|INFO|WARN|ERROR|NONE) +# - LAUNCHR_LOG_FORMAT: Controls log format (json|plain|pretty) +# - LAUNCHR_QUIET_MODE: Enables quiet mode (0|1) +# +# Command Line Flags Tested: +# - --log-level: Sets log level +# - --log-format: Sets log format +# - -v, -vv, -vvv, -vvvv: Verbosity levels +# - -q, --quiet: Quiet mode +# +# ============================================================================= + +# ============================================================================= +# Section 1: Test log formats using --log-format flag +# ============================================================================= + +# JSON format: Structured JSON output +exec launchr --log-format=json --log-level=INFO testplugin:log-levels +stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . +cp stdout json_format_flag_output.txt + +# Plain format: Key-value pairs with timestamp +exec launchr --log-format=plain --log-level=INFO testplugin:log-levels +stdout '^time=.*level=INFO.*msg=.*$' +stdout '^time=.*level=WARN.*msg=.*$' +stdout '^time=.*level=ERROR.*msg=.*$' +! stderr . +cp stdout plain_format_flag_output.txt + +# Pretty format: Human-readable with colors (need to account for ANSI codes) +exec launchr --log-format=pretty --log-level=INFO testplugin:log-levels +stdout '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*INFO.*this is INFO log' +stdout '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*WARN.*this is WARN log' +stdout '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*ERROR.*this is ERROR log' +! stderr . +cp stdout pretty_format_flag_output.txt + +# Default format (no --log-format): Should use pretty format +exec launchr --log-level=INFO testplugin:log-levels +cp stdout default_format_output.txt +# Remove ANSI color codes and timestamps for comparison +txtproc replace-regex '\x1b\[[0-9;]*m' '' default_format_output.txt default_no_color.txt +txtproc replace-regex '\x1b\[[0-9;]*m' '' pretty_format_flag_output.txt pretty_no_color.txt +txtproc replace-regex '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} ' '' default_no_color.txt default_normalized.txt +txtproc replace-regex '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} ' '' pretty_no_color.txt pretty_normalized.txt +cmp default_normalized.txt pretty_normalized.txt + +# ============================================================================= +# Section 2: Test log formats using LAUNCHR_LOG_FORMAT environment variable +# Verify environment variable produces identical output to flags +# ============================================================================= + +# Environment variable: JSON format +env LAUNCHR_LOG_FORMAT=json +exec launchr --log-level=INFO testplugin:log-levels +cp stdout json_format_env_output.txt +# Normalize JSON timestamps before comparison +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' json_format_flag_output.txt json_flag_normalized.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' json_format_env_output.txt json_env_normalized.txt +cmp json_flag_normalized.txt json_env_normalized.txt + +# Environment variable: Plain format +env LAUNCHR_LOG_FORMAT=plain +exec launchr --log-level=INFO testplugin:log-levels +cp stdout plain_format_env_output.txt +# Remove timestamps from both files for comparison - match beginning of each line +txtproc replace-regex 'time=[0-9T:.+-Z]+ ' '' plain_format_env_output.txt plain_env_normalized.txt +txtproc replace-regex 'time=[0-9T:.+-Z]+ ' '' plain_format_flag_output.txt plain_flag_normalized.txt +cmp plain_flag_normalized.txt plain_env_normalized.txt + +# Environment variable: Pretty format +env LAUNCHR_LOG_FORMAT=pretty +exec launchr --log-level=INFO testplugin:log-levels +cp stdout pretty_format_env_output.txt +txtproc replace-regex '\x1b\[[0-9;]*m' '' pretty_format_env_output.txt pretty_env_no_color.txt +txtproc replace-regex '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} ' '' pretty_env_no_color.txt pretty_env_normalized.txt +cmp pretty_normalized.txt pretty_env_normalized.txt + +# ============================================================================= +# Section 3: Test edge cases for LAUNCHR_LOG_FORMAT environment variable +# ============================================================================= + +# Edge case: Empty LAUNCHR_LOG_FORMAT should use pretty format (default) +env LAUNCHR_LOG_FORMAT= +exec launchr --log-level=INFO testplugin:log-levels +cp stdout empty_format_output.txt +txtproc replace-regex '\x1b\[[0-9;]*m' '' empty_format_output.txt empty_no_color.txt +txtproc replace-regex '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} ' '' empty_no_color.txt empty_format_normalized.txt +cmp pretty_normalized.txt empty_format_normalized.txt + +# Edge case: Undefined LAUNCHR_LOG_FORMAT should use pretty format (default) +env LAUNCHR_LOG_FORMAT= +exec launchr --log-level=INFO testplugin:log-levels +cp stdout undefined_format_output.txt +txtproc replace-regex '\x1b\[[0-9;]*m' '' undefined_format_output.txt undefined_no_color.txt +txtproc replace-regex '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} ' '' undefined_no_color.txt undefined_format_normalized.txt +cmp pretty_normalized.txt undefined_format_normalized.txt + +# ============================================================================= +# Section 4: Test log levels using --log-level flag (JSON format) +# ============================================================================= + +env LAUNCHR_LOG_FORMAT=json + +# DEBUG level: Should show all log messages +exec launchr --log-level=DEBUG testplugin:log-levels +stdout '^\{.*"level":"DEBUG".*\}$' +stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . +cp stdout debug_flag_output.txt + +# INFO level: Should show INFO, WARN, ERROR (no DEBUG) +exec launchr --log-level=INFO testplugin:log-levels +! stdout '^\{.*"level":"DEBUG".*\}$' +stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . +cp stdout info_flag_output.txt + +# WARN level: Should show WARN, ERROR only +exec launchr --log-level=WARN testplugin:log-levels +! stdout '^\{.*"level":"DEBUG".*\}$' +! stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . +cp stdout warn_flag_output.txt + +# ERROR level: Should show ERROR only +exec launchr --log-level=ERROR testplugin:log-levels +! stdout '^\{.*"level":"DEBUG".*\}$' +! stdout '^\{.*"level":"INFO".*\}$' +! stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . +cp stdout error_flag_output.txt + +# NONE level: Should show no log messages +exec launchr --log-level=NONE testplugin:log-levels +! stdout '^\{.*"level":"DEBUG".*\}$' +! stdout '^\{.*"level":"INFO".*\}$' +! stdout '^\{.*"level":"WARN".*\}$' +! stdout '^\{.*"level":"ERROR".*\}$' +! stderr . +cp stdout none_flag_output.txt + +# ============================================================================= +# Section 5: Test log levels using verbosity flags (-v, -vv, -vvv, -vvvv) +# ============================================================================= + +# -vvvv flag: Should be equivalent to DEBUG level +exec launchr -vvvv testplugin:log-levels +cp stdout debug_verbosity_output.txt +# Normalize JSON timestamps before comparison +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' debug_flag_output.txt debug_flag_normalized.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' debug_verbosity_output.txt debug_verbosity_normalized.txt +cmp debug_flag_normalized.txt debug_verbosity_normalized.txt + +# -vvv flag: Should be equivalent to INFO level +exec launchr -vvv testplugin:log-levels +cp stdout info_verbosity_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' info_flag_output.txt info_flag_normalized.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' info_verbosity_output.txt info_verbosity_normalized.txt +cmp info_flag_normalized.txt info_verbosity_normalized.txt + +# -vv flag: Should be equivalent to WARN level +exec launchr -vv testplugin:log-levels +cp stdout warn_verbosity_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' warn_flag_output.txt warn_flag_normalized.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' warn_verbosity_output.txt warn_verbosity_normalized.txt +cmp warn_flag_normalized.txt warn_verbosity_normalized.txt + +# -v flag: Should be equivalent to ERROR level +exec launchr -v testplugin:log-levels +cp stdout error_verbosity_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' error_flag_output.txt error_flag_normalized.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' error_verbosity_output.txt error_verbosity_normalized.txt +cmp error_flag_normalized.txt error_verbosity_normalized.txt + +# ============================================================================= +# Section 6: Test log levels using LAUNCHR_LOG_LEVEL environment variable +# Verify environment variable produces identical output to flags +# ============================================================================= + +# Environment variable: DEBUG level +env LAUNCHR_LOG_LEVEL=DEBUG +exec launchr testplugin:log-levels +cp stdout debug_env_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' debug_env_output.txt debug_env_normalized.txt +cmp debug_flag_normalized.txt debug_env_normalized.txt + +# Environment variable: INFO level +env LAUNCHR_LOG_LEVEL=INFO +exec launchr testplugin:log-levels +cp stdout info_env_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' info_env_output.txt info_env_normalized.txt +cmp info_flag_normalized.txt info_env_normalized.txt + +# Environment variable: WARN level +env LAUNCHR_LOG_LEVEL=WARN +exec launchr testplugin:log-levels +cp stdout warn_env_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' warn_env_output.txt warn_env_normalized.txt +cmp warn_flag_normalized.txt warn_env_normalized.txt + +# Environment variable: ERROR level +env LAUNCHR_LOG_LEVEL=ERROR +exec launchr testplugin:log-levels +cp stdout error_env_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' error_env_output.txt error_env_normalized.txt +cmp error_flag_normalized.txt error_env_normalized.txt + +# Environment variable: NONE level +env LAUNCHR_LOG_LEVEL=NONE +exec launchr testplugin:log-levels +cp stdout none_env_output.txt +cmp none_flag_output.txt none_env_output.txt + +# ============================================================================= +# Section 7: Test edge cases for LAUNCHR_LOG_LEVEL environment variable +# ============================================================================= + +# Edge case: Empty LAUNCHR_LOG_LEVEL should use default behavior +env LAUNCHR_LOG_LEVEL= +exec launchr testplugin:log-levels +cp stdout empty_env_output.txt + +# Edge case: Undefined LAUNCHR_LOG_LEVEL should use default behavior +env LAUNCHR_LOG_LEVEL= +exec launchr testplugin:log-levels +cp stdout undefined_env_output.txt + +# Verify empty and undefined produce the same output +cmp empty_env_output.txt undefined_env_output.txt + +# ============================================================================= +# Section 8: Test that different formats filter log levels consistently +# ============================================================================= + +# Test that plain format correctly filters WARN level +env LAUNCHR_LOG_FORMAT=plain +env LAUNCHR_LOG_LEVEL=WARN +exec launchr testplugin:log-levels +! stdout '^time=.*level=DEBUG.*$' +! stdout '^time=.*level=INFO.*$' +stdout '^time=.*level=WARN.*$' +stdout '^time=.*level=ERROR.*$' + +# Test that pretty format correctly filters WARN level (account for ANSI codes) +env LAUNCHR_LOG_FORMAT=pretty +env LAUNCHR_LOG_LEVEL=WARN +exec launchr testplugin:log-levels +! stdout 'DEBUG.*this is DEBUG log' +! stdout 'INFO.*this is INFO log' +stdout 'WARN.*this is WARN log' +stdout 'ERROR.*this is ERROR log' + +# Test that JSON format correctly filters WARN level +env LAUNCHR_LOG_FORMAT=json +env LAUNCHR_LOG_LEVEL=WARN +exec launchr testplugin:log-levels +! stdout '^\{.*"level":"DEBUG".*\}$' +! stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' + +# ============================================================================= +# Section 9: Test that all formats produce same number of log entries +# ============================================================================= + +# Test plain format at INFO level - should have 3 lines (INFO, WARN, ERROR) +env LAUNCHR_LOG_FORMAT=plain +env LAUNCHR_LOG_LEVEL=INFO +exec launchr testplugin:log-levels +cp stdout plain_info_test.txt +# Verify we have exactly 3 log lines +stdout '^time=.*level=INFO.*$' +stdout '^time=.*level=WARN.*$' +stdout '^time=.*level=ERROR.*$' + +# Test JSON format at INFO level - should have 3 lines (INFO, WARN, ERROR) +env LAUNCHR_LOG_FORMAT=json +env LAUNCHR_LOG_LEVEL=INFO +exec launchr testplugin:log-levels +cp stdout json_info_test.txt +# Verify we have exactly 3 log lines +stdout '^\{.*"level":"INFO".*\}$' +stdout '^\{.*"level":"WARN".*\}$' +stdout '^\{.*"level":"ERROR".*\}$' + +# Test pretty format at INFO level - should have 3 lines (INFO, WARN, ERROR) +env LAUNCHR_LOG_FORMAT=pretty +env LAUNCHR_LOG_LEVEL=INFO +exec launchr testplugin:log-levels +cp stdout pretty_info_test.txt +# Verify we have exactly 3 log lines +stdout 'INFO.*this is INFO log' +stdout 'WARN.*this is WARN log' +stdout 'ERROR.*this is ERROR log' + +# ============================================================================= +# Section 10: Additional JSON format tests with timestamp normalization +# ============================================================================= + +# Test JSON format with different methods and normalize timestamps +env LAUNCHR_LOG_FORMAT=json +env LAUNCHR_LOG_LEVEL=INFO + +# Test with flag override (should still be JSON due to env var) +exec launchr --log-level=INFO testplugin:log-levels +cp stdout json_combined_output.txt +txtproc replace-regex '"time":"[^"]*"' '"time":"NORMALIZED"' json_combined_output.txt json_combined_normalized.txt +cmp json_flag_normalized.txt json_combined_normalized.txt + +# ============================================================================= +# Section 11: Test quiet mode completely suppresses all output +# Even with DEBUG log level, quiet mode should produce no output +# ============================================================================= + +# Test -q flag with DEBUG level - should produce no output +env LAUNCHR_LOG_FORMAT=json +exec launchr -q --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +# Test --quiet flag with DEBUG level - should produce no output +env LAUNCHR_LOG_FORMAT=json +exec launchr --quiet --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +# Test LAUNCHR_QUIET_MODE environment variable with DEBUG level - should produce no output +env LAUNCHR_QUIET_MODE=1 +env LAUNCHR_LOG_FORMAT=json +exec launchr --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +# Test -q flag with different log formats - all should produce no output +env LAUNCHR_LOG_FORMAT=plain +exec launchr -q --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +env LAUNCHR_LOG_FORMAT=pretty +exec launchr -q --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +env LAUNCHR_LOG_FORMAT=json +exec launchr -q --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +# Test --quiet flag with different log formats - all should produce no output +env LAUNCHR_LOG_FORMAT=plain +exec launchr --quiet --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +env LAUNCHR_LOG_FORMAT=pretty +exec launchr --quiet --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +env LAUNCHR_LOG_FORMAT=json +exec launchr --quiet --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +# Test LAUNCHR_QUIET_MODE with different log formats - all should produce no output +env LAUNCHR_QUIET_MODE=1 +env LAUNCHR_LOG_FORMAT=plain +exec launchr --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +env LAUNCHR_QUIET_MODE=1 +env LAUNCHR_LOG_FORMAT=pretty +exec launchr --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +env LAUNCHR_QUIET_MODE=1 +env LAUNCHR_LOG_FORMAT=json +exec launchr --log-level=DEBUG testplugin:log-levels +! stdout . +! stderr . + +# Test that quiet mode overrides verbosity flags +exec launchr -q -vvvv testplugin:log-levels +! stdout . +! stderr . + +exec launchr --quiet -vvvv testplugin:log-levels +! stdout . +! stderr . + +env LAUNCHR_QUIET_MODE=1 +exec launchr -vvvv testplugin:log-levels +! stdout . +! stderr . + +# Test that quiet mode works with environment variable log level +env LAUNCHR_QUIET_MODE=1 +env LAUNCHR_LOG_LEVEL=DEBUG +exec launchr testplugin:log-levels +! stdout . +! stderr . + +# Test edge cases: LAUNCHR_QUIET_MODE=0 should allow output +env LAUNCHR_QUIET_MODE=0 +env LAUNCHR_LOG_FORMAT=json +exec launchr --log-level=ERROR testplugin:log-levels +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . + +# Test edge cases: Empty LAUNCHR_QUIET_MODE should allow output +env LAUNCHR_QUIET_MODE= +env LAUNCHR_LOG_FORMAT=json +exec launchr --log-level=ERROR testplugin:log-levels +stdout '^\{.*"level":"ERROR".*\}$' +! stderr . + +# Reset environment for subsequent tests +env LAUNCHR_QUIET_MODE= +env LAUNCHR_LOG_LEVEL= +env LAUNCHR_LOG_FORMAT= diff --git a/test/testdata/common/sensitive.txtar b/test/testdata/common/sensitive.txtar new file mode 100644 index 0000000..0088e92 --- /dev/null +++ b/test/testdata/common/sensitive.txtar @@ -0,0 +1,156 @@ +# ============================================================================= +# Launchr Sensitive Value Masking and Security Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Detect and mask sensitive values in various output streams +# 2. Protect sensitive data across different output methods +# 3. Maintain security while preserving functionality +# 4. Handle partial secret matching and variable substitution +# +# Test Structure: +# - Tests baseline behavior without secret masking +# - Tests complete secret masking with exact matches +# - Tests partial secret masking with substring matches +# - Validates masking across stdout, stderr, and log outputs +# - Tests different output methods (terminal, fmt, streams) +# ============================================================================= + +# Test 1: Baseline Behavior - No Secret Masking +# ----------------------------------------------------------------------------- +# Execute plugin with sensitive value but no environment secret configured +# This establishes baseline behavior where no masking should occur +exec launchr testplugin:sensitive -v 'MySuper SecretValue' + +# Validate terminal output shows unmasked value +# Without secret configuration, all outputs should display the actual value +stdout '^terminal output: MySuper SecretValue$' + +# Validate log output shows unmasked value with trailing whitespace +# Log entries may include additional formatting or timestamp information +stdout '.+ log output: MySuper SecretValue\s+' + +# Validate fmt.Print output shows unmasked value +# Direct fmt.Print calls should display the raw value without modification +stdout '^fmt print: MySuper SecretValue$' + +# Validate fmt stdout stream output shows unmasked value +# Stdout stream writes should display the raw value without modification +stdout '^fmt stdout streams print: MySuper SecretValue$' + +# Validate split output shows unmasked value +# Split or processed output should display the raw value without modification +stdout '^split output: MySuper SecretValue$' + +# Validate fmt stderr stream output shows unmasked value +# Stderr stream writes should display the raw value without modification +stderr '^fmt stderr streams print: MySuper SecretValue$' + +# Test 2: Complete Secret Masking - Exact Match +# ----------------------------------------------------------------------------- +# Configure environment variable with exact secret value for masking +# The TEST_SECRET environment variable defines the sensitive value to mask +env TEST_SECRET='MySuper SecretValue' + +# Execute plugin with the same value that matches the environment secret +# This should trigger complete masking of the sensitive value +exec launchr testplugin:sensitive -v 'MySuper SecretValue' + +# Validate terminal output shows completely masked value +# The entire secret should be replaced with asterisks for security +stdout '^terminal output: \*\*\*\*$' + +# Validate log output shows completely masked value with trailing whitespace +# Log entries should mask the secret while preserving formatting +stdout '.+ log output: \*\*\*\*\s+' + +# Validate fmt.Print output shows unmasked value +# Direct fmt.Print calls may bypass the masking system for internal use +stdout '^fmt print: MySuper SecretValue$' + +# Validate fmt stdout stream output shows completely masked value +# Stdout stream writes should be masked for security +stdout '^fmt stdout streams print: \*\*\*\*$' + +# Validate split output shows completely masked value +# Split or processed output should be masked for security +stdout '^split output: \*\*\*\*$' + +# Validate fmt stderr stream output shows completely masked value +# Stderr stream writes should be masked for security +stderr '^fmt stderr streams print: \*\*\*\*$' + +# Test 3: Partial Secret Masking - Substring Match +# ----------------------------------------------------------------------------- +# Configure environment variable with partial secret value for substring masking +# The TEST_SECRET contains only a portion of the full sensitive value +env TEST_SECRET='Super Secret' + +# Execute plugin with value containing the partial secret as substring +# This should trigger partial masking where only the matching portion is masked +exec launchr testplugin:sensitive -v 'MySuper SecretValue' + +# Validate terminal output shows partially masked value +# Only the matching substring should be replaced with asterisks +stdout '^terminal output: My\*\*\*\*Value$' + +# Validate log output shows partially masked value with trailing whitespace +# Log entries should mask only the matching substring while preserving format +stdout '.+ log output: My\*\*\*\*Value\s+' + +# Validate fmt.Print output shows unmasked value +# Direct fmt.Print calls may bypass the masking system for internal use +stdout '^fmt print: MySuper SecretValue$' + +# Validate fmt stdout stream output shows partially masked value +# Stdout stream writes should mask only the matching substring +stdout '^fmt stdout streams print: My\*\*\*\*Value$' + +# Validate split output shows partially masked value +# Split or processed output should mask only the matching substring +stdout '^split output: My\*\*\*\*Value$' + +# Validate fmt stderr stream output shows partially masked value +# Stderr stream writes should mask only the matching substring +stderr '^fmt stderr streams print: My\*\*\*\*Value$' + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Secret Detection and Masking: +# 1. Exact matches result in complete masking with asterisks +# 2. Partial matches result in substring masking with asterisks +# 3. Masking applies to most output streams for security +# 4. Some internal outputs (fmt.Print) may bypass masking for debugging +# +# Output Stream Behavior: +# - Terminal output: Masked according to secret configuration +# - Log output: Masked with potential additional formatting +# - Fmt print: Typically unmasked for internal/debug purposes +# - Stdout streams: Masked for security +# - Split output: Masked for security +# - Stderr streams: Masked for security +# +# Security Features: +# - Prevents accidental exposure of sensitive values in logs +# - Maintains functionality while protecting confidential data +# - Supports both complete and partial secret masking +# - Applies masking across multiple output channels +# - Uses asterisks as universal masking character +# +# Masking Algorithm: +# - Exact match: Replace entire value with **** +# - Substring match: Replace only matching portion with **** +# - Case-sensitive matching for precise secret detection +# - Preserves non-sensitive portions of the output +# - Maintains output structure and formatting +# +# Environment Configuration: +# - TEST_SECRET environment variable defines sensitive values +# - Supports multiple secret configurations simultaneously +# - Runtime configuration without code changes +# - Flexible secret definition for different scenarios +# - Environment-based security policy enforcement +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/runtime/container/basic.txtar b/test/testdata/runtime/container/basic.txtar new file mode 100644 index 0000000..e391546 --- /dev/null +++ b/test/testdata/runtime/container/basic.txtar @@ -0,0 +1,213 @@ +# ============================================================================= +# Launchr Container Runtime Features Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's container runtime +# features and execution capabilities: +# 1. Environment variable handling (static and dynamic from host) +# 2. Extra hosts configuration for container networking +# 3. Output stream handling (stdout/stderr separation) +# 4. Command execution override functionality +# 5. Entrypoint override capabilities +# 6. Container process lifecycle management +# +# Test Structure: +# - Tests environment variable injection and templating +# - Tests custom host entries in container /etc/hosts +# - Tests proper output stream routing +# - Tests command and entrypoint override mechanisms +# - Validates container runtime integration features +# ============================================================================= + +# Setup Phase: Host Environment Variables +# ----------------------------------------------------------------------------- +# Set host environment variables for dynamic injection testing +env HOST_ENV_1=bar # Host variable for template substitution +env HOST_ENV_2=buz # Host variable (not used in action) + +# Test 1: Environment Variable Injection and Templating +# ----------------------------------------------------------------------------- +# Execute action that tests both static and dynamic environment variables +exec launchr test-env-vars + +# Validate static environment variable injection +# ACTION_ENV1 should be set to static value "foo" +# ACTION_ENV2 should be dynamically set from HOST_ENV_1 value "bar" +stdout '^ACTION_ENV1=foo ACTION_ENV2=bar$' + +# Validate host environment variable isolation +# Host variables should NOT be available inside container by default +stdout '^HOST_ENV_1= HOST_ENV_2=$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Extra Hosts Configuration +# ----------------------------------------------------------------------------- +# Execute action that tests custom /etc/hosts entries in container +exec launchr test-extra-hosts + +# Validate host-gateway mapping (Docker internal host resolution) +stdout '^[0-9.]+\s+host\.docker\.internal$' + +# Validate custom host mapping (static IP assignment) +stdout '^127\.1\.2\.3\s+example\.com$' + +# Validate clean execution +! stderr . + +# Test 3: Output Stream Handling +# ----------------------------------------------------------------------------- +# Execute action that writes to both stdout and stderr streams +exec launchr test-output + +# Validate stdout output routing +stdout '^output to stdout$' + +# Validate stderr output routing +stderr '^output to stderr$' + +# Test 4: Default Command Execution +# ----------------------------------------------------------------------------- +# Execute action with default command configuration +exec launchr test-exec + +# Validate default action command execution +stdout '^action command$' + +# Validate clean execution +! stderr . + +# Test 5: Command Override with --exec Flag +# ----------------------------------------------------------------------------- +# Execute action with command override using --exec flag +exec launchr test-exec --exec -- echo 'exec command' + +# Validate overridden command execution +stdout '^exec command$' + +# Validate original command is NOT executed +! stdout '^action command$' + +# Validate clean execution +! stderr . + +# Test 6: Entrypoint Override with --exec and --entrypoint +# ----------------------------------------------------------------------------- +# Execute action with both entrypoint and command override +exec launchr test-exec --exec --entrypoint 'echo' -- 'entrypoint command' + +# Validate entrypoint override execution +stdout '^entrypoint command$' + +# Validate original command is NOT executed +! stdout '^action command$' + +# Validate clean execution +! stderr . + +# ============================================================================= +# Test Data Files - Action Configurations and Scripts +# ============================================================================= + +# Environment Variables Test Action +-- actions/test-env-vars/action.yaml -- +# Container action demonstrating environment variable injection +action: + title: envvars # Human-readable action name + description: Test passing static or dynamic environment variables to container + +runtime: + type: container # Container execution type + image: alpine:latest # Base container image + env: + ACTION_ENV1: foo # Static environment variable + ACTION_ENV2: ${HOST_ENV_1} # Dynamic variable from host environment + command: + - sh # Shell interpreter + - -c # Execute command string + - | # Multi-line command block + echo "ACTION_ENV1=$${ACTION_ENV1} ACTION_ENV2=$${ACTION_ENV2}" + echo "HOST_ENV_1=$${HOST_ENV_1} HOST_ENV_2=$${HOST_ENV_2}" + +# Extra Hosts Test Action +-- actions/test-extra-hosts/action.yaml -- +# Container action demonstrating custom /etc/hosts entries +action: + title: extrahosts # Human-readable action name + description: Test passing additional entries to container''s /etc/hosts + +runtime: + type: container # Container execution type + image: alpine:latest # Base container image + extra_hosts: + - "host.docker.internal:host-gateway" # Docker internal host resolution + - "example.com:127.1.2.3" # Custom host to IP mapping + command: + - cat # Display file contents + - /etc/hosts # Container hosts file + +# Command Execution Test Action +-- actions/test-exec/action.yaml -- +# Container action for testing command execution and override +action: + title: test exec override # Human-readable action name + +runtime: + type: container # Container execution type + image: alpine:latest # Base container image + command: + - echo # Echo command + - "action command" # Default command output + +# Output Stream Test Action +-- actions/test-output/action.yaml -- +# Container action for testing stdout/stderr output routing +action: + title: test exec override # Human-readable action name + +runtime: + type: container # Container execution type + image: alpine:latest # Base container image + command: + - sh # Shell interpreter + - -c # Execute command string + - | # Multi-line command block + echo "output to stdout" # Write to standard output + echo "output to stderr" >&2 # Write to standard error + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Environment Variable Handling: +# - Static variables are injected directly into container environment +# - Dynamic variables use ${VAR} syntax for host environment substitution +# - Host environment variables are isolated from container by default +# - Template substitution occurs before container execution +# +# Extra Hosts Configuration: +# - Custom entries are added to container's /etc/hosts file +# - host-gateway resolves to Docker host IP address +# - Static IP mappings allow custom hostname resolution +# - Multiple host entries can be configured per action +# +# Output Stream Management: +# - Container stdout is routed to launchr stdout +# - Container stderr is routed to launchr stderr +# - Output streams are properly separated and preserved +# - No output mixing or corruption occurs +# +# Command Override Features: +# - --exec flag allows runtime command replacement +# - --entrypoint flag allows entrypoint override +# - Original action commands are bypassed when overridden +# - Override commands receive proper argument passing +# +# Container Runtime Integration: +# - All features work with standard container images +# - No special container modifications required +# - Proper resource cleanup after execution +# - Error handling and reporting maintained +# +# ============================================================================= diff --git a/test/testdata/runtime/container/image-build.txtar b/test/testdata/runtime/container/image-build.txtar new file mode 100644 index 0000000..ee0e32d --- /dev/null +++ b/test/testdata/runtime/container/image-build.txtar @@ -0,0 +1,320 @@ +# ============================================================================= +# Launchr Container Image Build and Caching Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Build container images dynamically when they don't exist locally +# 2. Cache built images and skip rebuilding when images exist +# 3. Force rebuild images using --no-cache flag +# 4. Clean up images automatically with --remove-image flag +# 5. Handle custom build contexts, Dockerfiles, and build arguments +# 6. Apply multiple tags to built images +# 7. Use embedded filesystem actions for image building +# 8. Track image checksums in actions.sum file +# +# Test Structure: +# - Tests automatic image building on first run +# - Tests image caching behavior on subsequent runs +# - Tests forced rebuild with --no-cache flag +# - Tests image cleanup with --remove-image flag +# - Tests custom build contexts and Dockerfiles +# - Tests build argument templating and environment variables +# - Tests multiple image tagging +# - Tests embedded filesystem action integration +# - Validates checksum tracking and cleanup operations +# ============================================================================= + +# Prepare a randomized image registry for builds. +# This ensures unique registry names in shared environments, +# such as GitHub Actions on Windows/macOS, or in Kubernetes clusters. +env IMAGE_REGISTRY=$RANDOM +txtproc replace '[img_registy]' $IMAGE_REGISTRY actions/testimage-1/action.yaml actions/testimage-1/action.yaml +txtproc replace '[img_registy]' $IMAGE_REGISTRY actions/testimage-2/action.yaml actions/testimage-2/action.yaml +txtproc replace '[img_registy]' $IMAGE_REGISTRY actions/testimage-3/action.yaml actions/testimage-3/action.yaml +txtproc replace '[img_registy]' $IMAGE_REGISTRY .launchr/config.yaml .launchr/config.yaml + +# Test 1: Initial Image Build with Multiple Tags +# ----------------------------------------------------------------------------- +# Execute action that requires building a new image with multiple tags +exec launchr testimage-1 + +# Validate initial build output messages +stdout '^Image "'$IMAGE_REGISTRY'/testimage-1:latest" doesn''t exist locally, building...$' +stdout '^Successfully built .+$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:latest$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:bar$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:buz$' + +# Validate container execution output (user ID verification) +stdout '^uid=\d+\(foobar\) gid=\d+(\(.+\))? groups=\d+(\(.+\))?$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Image Caching Behavior +# ----------------------------------------------------------------------------- +# Execute same action again to test caching (should not rebuild) +exec launchr testimage-1 + +# Validate that rebuild messages do NOT appear (image cached) +! stdout '^Image "'$IMAGE_REGISTRY'/testimage-1:latest" doesn''t exist locally, building...$' +! stdout '^Successfully built .+$' +! stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:latest$' +! stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:bar$' +! stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:buz$' + +# Validate container execution still works (using cached image) +stdout '^uid=\d+\(foobar\) gid=\d+(\(.+\))? groups=\d+(\(.+\))?$' + +# Validate clean execution +! stderr . + +# Test 3: Forced Rebuild with --no-cache Flag +# ----------------------------------------------------------------------------- +# Execute action with --no-cache to force rebuild despite existing image +exec launchr testimage-1 --no-cache + +# Validate forced rebuild output messages +stdout '^Image "'$IMAGE_REGISTRY'/testimage-1:latest" doesn''t exist locally, building...$' +stdout '^Successfully built .+$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:latest$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:bar$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-1:buz$' + +# Validate container execution after forced rebuild +stdout '^uid=\d+\(foobar\) gid=\d+(\(.+\))? groups=\d+(\(.+\))?$' + +# Validate clean execution +! stderr . + +# Test 4: Image Cleanup with --remove-image Flag +# ----------------------------------------------------------------------------- +# Execute action with --remove-image to automatically clean up after execution +exec launchr testimage-2 --remove-image + +# Validate build and execution output +stdout '^Image "'$IMAGE_REGISTRY'/testimage-2:foo" doesn''t exist locally, building...$' +stdout '^Successfully built .+$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-2:foo$' +stdout '^uid=\d+\(foobar\) gid=\d+(\(.+\))? groups=\d+(\(.+\))?$' + +# Validate clean execution +! stderr . + +# Test 5: Verify Image Cleanup Worked +# ----------------------------------------------------------------------------- +# Verify that image was actually removed after --remove-image execution +! exec docker image inspect ${IMAGE_REGISTRY}/testimage-2:foo --format '{{ .Id }}' +stderr 'No such image: '$IMAGE_REGISTRY'/testimage-2:foo' + +# Test 6: Rebuild After Cleanup +# ----------------------------------------------------------------------------- +# Execute same action again to verify it rebuilds after cleanup +exec launchr testimage-2 + +# Validate rebuild occurs (image was successfully removed) +stdout '^Image "'$IMAGE_REGISTRY'/testimage-2:foo" doesn''t exist locally, building...$' +stdout '^Successfully built .+$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-2:foo$' +stdout '^uid=\d+\(foobar\) gid=\d+(\(.+\))? groups=\d+(\(.+\))?$' + +# Validate clean execution +! stderr . + +# Test 7: Custom Build Configuration via Global Config +# ----------------------------------------------------------------------------- +# Execute action that uses global image configuration from .launchr/config.yaml +exec launchr testimage-3 + +# Validate build output with custom configuration +stdout '^Image "'$IMAGE_REGISTRY'/testimage-3" doesn''t exist locally, building...$' +stdout '^Successfully built .+$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-3:latest$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-3:bar$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-3:buz$' + +# Validate build argument templating (environment variables passed correctly) +stdout '^MY_ARG_1=foo MY_ARG_2=bar$' + +# Validate clean execution +! stderr . + +# Test 8: Embedded Filesystem Action Integration +# ----------------------------------------------------------------------------- +# Execute embedded filesystem action that builds container image +exec launchr test-registered-embed-fs:container-image-build + +# Validate embedded action build output +stdout '^Image "'$IMAGE_REGISTRY'/testimage-embed:latest" doesn''t exist locally, building...$' +stdout '^Successfully built .+$' +stdout '^Successfully tagged '$IMAGE_REGISTRY'/testimage-embed:latest$' + +# Validate container execution from embedded action +stdout '^uid=\d+\(foobar\) gid=\d+(\(.+\))? groups=\d+(\(.+\))?$' + +# Validate file operations from embedded action +stdout '^action ls: Dockerfile action\.yaml container\.txt main\.sh$' +stdout '^host ls: actions container\.txt$' + +# Validate file content written by container to host +grep '^hello host from container$' ./container.txt + +# Validate clean execution +! stderr . + +# Test 9: Checksum Tracking Validation +# ----------------------------------------------------------------------------- +# Verify that all built images are properly tracked in actions.sum file +grep '^'$IMAGE_REGISTRY'\/testimage-1:latest h1:' ./.launchr/actions.sum +grep '^'$IMAGE_REGISTRY'\/testimage-1:bar h1:' ./.launchr/actions.sum +grep '^'$IMAGE_REGISTRY'\/testimage-1:buz h1:' ./.launchr/actions.sum +grep '^'$IMAGE_REGISTRY'\/testimage-2:foo h1:' ./.launchr/actions.sum +grep '^'$IMAGE_REGISTRY'\/testimage-3 h1:' ./.launchr/actions.sum +grep '^'$IMAGE_REGISTRY'\/testimage-3:bar h1:' ./.launchr/actions.sum +grep '^'$IMAGE_REGISTRY'\/testimage-3:buz h1:' ./.launchr/actions.sum +grep '^'$IMAGE_REGISTRY'\/testimage-embed:latest h1:' ./.launchr/actions.sum + +# Test Cleanup Phase: Remove Generated Images +# ----------------------------------------------------------------------------- +# Clean up all generated images to avoid interfering with other tests +exec docker rmi -f ${IMAGE_REGISTRY}/testimage-1:latest ${IMAGE_REGISTRY}/testimage-1:bar ${IMAGE_REGISTRY}/testimage-1:buz +exec docker rmi -f ${IMAGE_REGISTRY}/testimage-2:foo +exec docker rmi -f ${IMAGE_REGISTRY}/testimage-3 ${IMAGE_REGISTRY}/testimage-3:bar ${IMAGE_REGISTRY}/testimage-3:buz +exec docker rmi -f ${IMAGE_REGISTRY}/testimage-embed:latest + +# ============================================================================= +# Test Data Files - Action Configurations and Build Files +# ============================================================================= + +# Test Image 1: Build Arguments and Multiple Tags +-- actions/testimage-1/action.yaml -- +# Container action demonstrating build arguments and multiple image tags +action: + title: buildargs # Human-readable action name + description: Test passing args to Dockerfile + +runtime: + type: container # Container execution type + image: [img_registy]/testimage-1:latest # Primary image name + build: + context: ./ # Build context directory + args: + USER_ID: $UID # Template: current user ID + GROUP_ID: $GID # Template: current group ID + USER_NAME: foobar # Static build argument + tags: + - [img_registy]/testimage-1:bar # Additional image tag + - [img_registy]/testimage-1:buz # Additional image tag + command: + - id # Command to display user info + +# Test Image 1: Dockerfile with Build Arguments +-- actions/testimage-1/Dockerfile -- +# Dockerfile demonstrating build argument usage for user creation +FROM alpine:latest +ARG USER_ID # Build argument for user ID +ARG USER_NAME # Build argument for username +ARG GROUP_ID # Build argument for group ID +RUN adduser -D -u ${USER_ID} -g ${GROUP_ID} ${USER_NAME} || true +USER ${USER_NAME} # Switch to created user + +# Test Image 2: Custom Build Context and Dockerfile +-- actions/testimage-2/action.yaml -- +# Container action demonstrating custom build context and Dockerfile name +action: + title: buildargs # Human-readable action name + description: Test passing args to Dockerfile + +runtime: + type: container # Container execution type + image: [img_registy]/testimage-2:foo # Custom image name with tag + build: + context: ./context # Custom build context subdirectory + buildfile: test.Dockerfile # Custom Dockerfile name + args: + USER_ID: {{ .current_uid }} # Template: current user ID + GROUP_ID: {{ .current_gid }} # Template: current group ID + USER_NAME: foobar # Static build argument + command: + - id # Command to display user info + +# Test Image 2: Custom Named Dockerfile +-- actions/testimage-2/context/test.Dockerfile -- +# Custom named Dockerfile in subdirectory build context +FROM alpine:latest +ARG USER_ID # Build argument for user ID +ARG USER_NAME # Build argument for username +ARG GROUP_ID # Build argument for group ID +RUN adduser -D -u ${USER_ID} -g ${GROUP_ID} ${USER_NAME} || true +USER ${USER_NAME} # Switch to created user + +# Test Image 3: Global Configuration Reference +-- actions/testimage-3/action.yaml -- +# Container action that uses global image configuration +action: + title: buildargs # Human-readable action name + description: Test passing args to Dockerfile + +runtime: + type: container # Container execution type + image: [img_registy]/testimage-3 # Image name (config in .launchr/config.yaml) + command: + - sh # Shell command execution + - -c # Execute command string + - echo "MY_ARG_1=$${MY_ARG_1} MY_ARG_2=$${MY_ARG_2}" # Display build args + +# Global Image Configuration +-- .launchr/config.yaml -- +# Global configuration file defining image build settings +images: + [img_registy]/testimage-3: # Image name matching action + context: ./testimage-3 # Build context directory + buildfile: test1.Dockerfile # Custom Dockerfile name + args: + MY_ARG_1: "foo" # Build argument value + MY_ARG_2: "bar" # Build argument value + tags: + - [img_registy]/testimage-3:bar # Additional image tag + - [img_registy]/testimage-3:buz # Additional image tag + +# Global Configuration Dockerfile +-- .launchr/testimage-3/test1.Dockerfile -- +# Dockerfile for globally configured image with environment variables +FROM alpine:latest +ARG MY_ARG_1 +ARG MY_ARG_2 +ENV MY_ARG_1=${MY_ARG_1} +ENV MY_ARG_2=${MY_ARG_2} + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Image Build Process: +# 1. Check if image exists locally before building +# 2. Build image using specified context and Dockerfile +# 3. Apply build arguments with template substitution +# 4. Tag image with primary name and additional tags +# 5. Execute container command after successful build +# 6. Track image checksums in .launchr/actions.sum file +# +# Caching Behavior: +# - Images are cached after first build +# - Subsequent runs skip building if image exists +# - --no-cache flag forces rebuild regardless of cache +# - --remove-image flag cleans up image after execution +# +# Configuration Options: +# - Inline build configuration in action.yaml +# - Global image configuration in .launchr/config.yaml +# - Custom build contexts and Dockerfile names +# - Build argument templating with system variables +# - Multiple image tagging support +# +# Integration Features: +# - Embedded filesystem action support +# - Checksum tracking for cache validation +# - Automatic cleanup and rebuild capabilities +# - Template variable substitution ($UID, current_uid, current_gid, $GID) +# +# ============================================================================= diff --git a/test/testdata/runtime/container/kill.txtar b/test/testdata/runtime/container/kill.txtar new file mode 100644 index 0000000..109a142 --- /dev/null +++ b/test/testdata/runtime/container/kill.txtar @@ -0,0 +1,151 @@ +# ============================================================================= +# Launchr Signal Forwarding and Process Management Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Execute long-running containerized actions in background mode +# 2. Forward system signals (SIGINT) to running container processes +# 3. Handle graceful process termination and cleanup +# 4. Capture and report custom exit codes from signal handlers +# 5. Manage background process lifecycle and synchronization +# +# Test Structure: +# - Tests background execution of long-running container actions +# - Tests signal forwarding from host to container process +# - Tests custom signal handling and exit code reporting +# - Validates proper process cleanup and output handling +# - Ensures cross-platform compatibility (Unix-like systems only) +# ============================================================================= + +# Platform Compatibility Check +# ----------------------------------------------------------------------------- +# Signal forwarding and process management testing is not supported on Windows +# Skip this entire test suite when running on Windows systems +[windows] skip 'testing kill is not supported on windows' + +# Make sure alpine image is available during tests, it's crucial for timings. +exec docker pull alpine:latest + +# Test: Signal Forwarding and Process Management +# ----------------------------------------------------------------------------- +# This test validates the complete signal forwarding pipeline: +# 1. Background execution of containerized action +# 2. Signal transmission from host to container +# 3. Custom signal handling within container +# 4. Proper exit code propagation and reporting + +# Execute long-running action and test signal forwarding +# Start the test-signal action in background mode (&appint& syntax) +# This allows the test to continue while the action runs asynchronously +! exec launchr test-signal &appint& + +# Allow container process to start and begin waiting +# Give the container sufficient time to: +# - Initialize the runtime environment +# - Execute the signal handling script +# - Set up signal traps and enter waiting state +sleep 5s + +# Send interrupt signal to test signal forwarding +# Transmit SIGINT (interrupt signal) to the background process +# This tests the signal forwarding mechanism between host and container +kill -INT appint + +# Wait for process to complete and validate exit behavior +# Synchronize with the background process to ensure completion +# This blocks until the signal handler finishes execution +wait appint + +# Output Validation: Signal Reception and Handling +# ----------------------------------------------------------------------------- +# Validate signal reception and handling within container +# Expected output: "Received signal: SIGINT" from the signal trap +stdout '^Waiting for signals...$' +stdout '^Received signal: SIGINT$' + +# Validate proper exit code handling (custom exit code 42) +# Expected output: completion message with exit code 42 +# This confirms that custom exit codes are properly propagated +stdout 'action "test-signal" finished with exit code 42' + +# Validate clean execution (no stderr output) +# Ensure no error output is generated during signal handling +# This confirms proper error handling and clean process termination +! stderr . + +# ============================================================================= +# Test Data Files - Action Configurations and Scripts +# ============================================================================= + +# Signal Forwarding Test Action Configuration +# ----------------------------------------------------------------------------- +# This file defines the containerized action for signal forwarding testing +# Uses Alpine Linux container with custom signal handling script +-- actions/test-signal/action.yaml -- +# Container action for testing signal forwarding and process management +action: + title: test signal forwarding # Human-readable action name + description: Test signal forwarding # Action description + +runtime: + type: container # Container execution type + image: alpine:latest # Base container image (Alpine Linux) + command: + - sh # Shell interpreter + - /action/main.sh # Execute signal handling script + +# Signal Handling Script Implementation +# ----------------------------------------------------------------------------- +# This shell script demonstrates proper signal handling within containers +# Implements custom signal traps and exit code management +-- actions/test-signal/main.sh -- +#!/bin/sh + +trap 'echo "Received signal: SIGINT"; exit 42' INT +echo "Waiting for signals..." + +sleep 10 +exit 45 + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Signal Forwarding Rules: +# 1. Background processes must be properly managed and trackable +# 2. System signals must be forwarded from host to container +# 3. Container signal handlers must execute correctly +# 4. Custom exit codes must be preserved and reported +# 5. Process cleanup must occur without resource leaks +# +# Background Process Management: +# - Process identifier: &appint& syntax for background execution +# - Signal targeting: kill -INT appint for signal delivery +# - Process synchronization: wait appint for completion blocking +# - Exit code propagation: Custom codes preserved through container boundary +# +# Signal Handling Workflow: +# 1. Container starts and sets up signal traps +# 2. Container enters waiting state (sleep 10) +# 3. Host sends SIGINT to container process +# 4. Container trap handler executes custom logic +# 5. Container exits with custom code (42) +# 6. Host reports completion with exit code +# +# Output Validation: +# - Signal reception: "Received signal: SIGINT" message +# - Exit code reporting: "action finished with exit code 42" message +# - Clean execution: No stderr output from launchr or container +# +# Platform Support: +# - Full support on Unix-like systems (Linux, macOS) +# - Not supported on Windows platforms (signal handling differences) +# - Graceful skip behavior for unsupported platforms +# +# Container Runtime: +# - Uses Alpine Linux base image for minimal overhead +# - Executes custom shell scripts via /bin/sh +# - Proper signal forwarding through container runtime +# - Isolated execution environment with host signal integration +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/runtime/container/remote.txtar b/test/testdata/runtime/container/remote.txtar new file mode 100644 index 0000000..a0a0c1e --- /dev/null +++ b/test/testdata/runtime/container/remote.txtar @@ -0,0 +1,281 @@ +# ============================================================================= +# Launchr Remote Runtime and File Synchronization Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's remote runtime +# capabilities and file synchronization features: +# 1. Custom working directory configuration for actions +# 2. Remote runtime execution with --remote-runtime flag +# 3. File synchronization between host and remote environments +# 4. Bidirectional file copy behavior with --remote-copy-back +# 5. Embedded filesystem action execution in remote contexts +# 6. Working directory isolation and file management +# 7. Concurrent execution and file system state handling +# +# Test Structure: +# - Tests local execution with custom working directory +# - Tests remote execution with file synchronization +# - Tests remote copy-back functionality +# - Tests embedded filesystem actions in remote contexts +# - Tests file system state management during concurrent operations +# - Validates proper cleanup and isolation between execution modes +# ============================================================================= + +# Setup test environment with custom working directory +mkdir remote-wd-1 remote-wd-2 remote-wd-3 +cp dummydata/host.txt remote-wd-1/host.txt +cp dummydata/host.txt remote-wd-2/host.txt +cp dummydata/host.txt remote-wd-3/host.txt + +# Ensure docker can modify files in the TMP directory on Windows. +[windows] exec icacls $WORK /grant Everyone:M /T + +# Test 1: Local Execution with Custom Working Directory +# ----------------------------------------------------------------------------- +# Execute action in background for concurrent file operations testing +env CUSTOM_WD=remote-wd-1 +exec launchr test-remote & + +# Allow container to start and read initial file +sleep 2s + +# Add second file while container is running (tests file system sync) +cp dummydata/host2.txt $CUSTOM_WD/host2.txt + +# Wait for background execution to complete +wait + +# Validate container read initial host file +stdout '^hello from host$' + +# Validate container read second host file (added during execution) +stdout '^hello from 2nd host$' + +# Validate container wrote output file to host working directory +exists $CUSTOM_WD/container.txt + +# Validate container removed the second host file during execution +! exists $CUSTOM_WD/host2.txt + +# Validate container output file contents +grep '^hello from container$' $CUSTOM_WD/container.txt + +# Validate clean execution (no error output) +! stderr . + +# Test 2: Remote Runtime Execution with File Synchronization +# ----------------------------------------------------------------------------- +# Execute action in remote runtime mode (files synchronized to remote) +env CUSTOM_WD=remote-wd-2 +exec launchr test-remote --remote-runtime & + +# Allow remote container to start and synchronize files +sleep 2s + +# Add second file to host working directory during remote execution +cp dummydata/host2.txt $CUSTOM_WD/host2.txt + +# Wait for remote execution to complete +wait + +# Validate remote runtime execution indicator +stdout 'Running in the remote environment' + +# Validate remote container read initial synchronized file +stdout '^hello from host$' + +# Validate remote container did NOT see file added during execution +# (Remote environment doesn't sync files added after initial sync) +stdout 'no file host2.txt' + +# Validate host file added during execution still exists +# (Remote execution doesn't affect host file system) +exists $CUSTOM_WD/host2.txt + +# Validate container output file was NOT copied back to host +# (Default remote behavior doesn't copy back unless specified) +! exists $CUSTOM_WD/container.txt + +# Validate clean execution +! stderr . + +# Test 3: Remote Runtime with Copy-Back Functionality +# ----------------------------------------------------------------------------- +# Execute action in remote runtime mode with copy-back enabled +env CUSTOM_WD=remote-wd-3 +exec launchr test-remote --remote-runtime --remote-copy-back & + +# Allow remote container to start and synchronize files +sleep 2s + +# Add second file to host working directory during remote execution +cp dummydata/host2.txt $CUSTOM_WD/host2.txt + +# Wait for remote execution to complete +wait + +# Validate remote runtime execution indicator +stdout 'Running in the remote environment' + +# Validate copy-back flag was recognized and processed +stdout '"--remote-copy-back" is set' + +# Validate remote container read initial synchronized file +stdout '^hello from host$' + +# Validate remote container did NOT see file added during execution +stdout 'no file host2.txt' + +# TODO: Fix this case - copy back should delete the file inside the container +# and synchronize back to host, removing files that were deleted remotely +# Currently this test is commented out due to known limitation. +#! exists $CUSTOM_WD/host2.txt + +# Validate container output file was copied back to host +exists $CUSTOM_WD/container.txt + +# Validate copied-back file contents +grep '^hello from container$' $CUSTOM_WD/container.txt + +# Validate clean execution +! stderr . + +# Test 4: Embedded Filesystem Action in Local Mode +# ----------------------------------------------------------------------------- +# Execute embedded filesystem action in local mode +exec launchr test-embed-fs:container + +# Validate embedded action can list its own files +stdout '^action ls: action\.yaml container\.txt main\.sh$' + +# Validate embedded action can see host file system +stdout '^host ls: actions container\.txt dummydata remote-wd-1 remote-wd-2 remote-wd-3$' + +# Validate embedded action can write to host file system +grep '^hello host from container$' ./container.txt + +# Validate clean execution +! stderr . + +# Cleanup generated file +rm ./container.txt + +# Test 5: Embedded Filesystem Action in Remote Mode +# ----------------------------------------------------------------------------- +# Execute embedded filesystem action in remote runtime mode +exec launchr test-embed-fs:container --remote-runtime + +# Validate embedded action can list its own files in remote environment +stdout '^action ls: action\.yaml container\.txt main\.sh$' + +# Validate embedded action can see host file system in remote environment +stdout '^host ls: actions container\.txt dummydata remote-wd-1 remote-wd-2 remote-wd-3$' + +# Validate embedded action output file was NOT copied back to host +# (Remote execution without copy-back doesn't affect host file system) +! exists ./container.txt + +# Validate clean execution +! stderr . + +# ============================================================================= +# Test Data Files - Action Configurations and Scripts +# ============================================================================= + +# Remote Runtime Test Action +-- actions/test-remote/action.yaml -- +# Container action demonstrating remote runtime and custom working directory +working_directory: ${CUSTOM_WD} # Custom working directory for action +action: + title: remote # Human-readable action name + description: Test --remote-runtime # Action description + +runtime: + type: container # Container execution type + image: alpine:latest # Base container image + command: + - sh # Shell interpreter + - /action/main.sh # Execute main script + +# Remote Runtime Test Script +-- actions/test-remote/main.sh -- +#!/bin/sh +# Script demonstrating file operations in remote runtime environment +ls -al + +# Read and display initial host file +cat ./host.txt + +# Wait briefly to allow concurrent file operations testing +echo "wait host" && sleep 2s + +# Attempt to read second host file (may not exist in remote environment) +cat ./host2.txt 2>/dev/null || echo "no file host2.txt" + +# Remove second host file if it exists (test file manipulation) +rm -f ./host2.txt 2>/dev/null || echo "failed to delete host2.txt" + +# Create output file to test copy-back functionality +echo "hello from container" > ./container.txt || echo "failed to write container.txt" + +# Test Data Files +# ----------------------------------------------------------------------------- + +# Initial Host File +-- ./dummydata/host.txt -- +hello from host + +# Second Host File (Added During Execution) +-- ./dummydata/host2.txt -- +hello from 2nd host + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Working Directory Configuration: +# - Actions can specify custom working directories +# - Working directory is created if it doesn't exist +# - All file operations are relative to the working directory +# - Working directory isolation prevents conflicts between actions +# +# Local Execution Mode (Default): +# - Actions execute in local container runtime +# - Working directory is mounted directly into container +# - File changes are immediately visible to both host and container +# - Files added/removed during execution are synchronized in real-time +# - Container output files are written directly to host file system +# +# Remote Runtime Mode (--remote-runtime): +# - Actions execute in remote container environment +# - Working directory is synchronized to remote environment at start +# - Files added to host during execution are NOT visible to remote container +# - Remote file changes do NOT affect host file system by default +# - Provides isolation between host and remote execution environments +# +# Remote Copy-Back Mode (--remote-copy-back): +# - Enables copying files from remote environment back to host +# - Files created/modified in remote environment are synchronized back +# - Files deleted in remote environment should be removed from host (TODO) +# - Bidirectional synchronization at action completion +# +# Embedded Filesystem Actions: +# - Actions can be embedded in the launchr binary +# - Work with both local and remote execution modes +# - Can access and modify host file system when permitted +# - Provide consistent behavior across execution environments +# +# File Synchronization Behavior: +# - Initial synchronization occurs before action execution +# - Real-time synchronization in local mode +# - Batch synchronization in remote mode +# - Copy-back synchronization after remote execution +# - Proper handling of file creation, modification, and deletion +# +# Concurrent Execution Support: +# - Actions can run concurrently with background execution +# - File system operations are properly isolated +# - No conflicts between concurrent action executions +# - Proper cleanup and resource management +# +# ============================================================================= diff --git a/test/testdata/runtime/container/tty.txtar b/test/testdata/runtime/container/tty.txtar new file mode 100644 index 0000000..0f435bc --- /dev/null +++ b/test/testdata/runtime/container/tty.txtar @@ -0,0 +1,133 @@ +# ============================================================================= +# Launchr TTY (Terminal) Integration Test Suite +# ============================================================================= +# +# This comprehensive test file validates the Launchr tool's ability to: +# 1. Handle TTY (terminal) input/output operations +# 2. Process interactive terminal sessions with container runtimes +# 3. Manage environment variables within containerized actions +# 4. Handle both successful and failing terminal sessions +# 5. Properly capture and display terminal output and error codes +# +# Test Structure: +# - Tests successful TTY interaction with environment variables +# - Tests TTY session with non-zero exit codes and stderr output +# - Validates proper output formatting and error handling +# - Ensures cross-platform compatibility (except Windows) +# ============================================================================= + +# Platform Compatibility Check +# ----------------------------------------------------------------------------- +# Testing of TTY functionality is not supported on Windows platforms +# Skip this entire test suite when running on Windows systems +[windows] skip 'Testing TTY is not supported on Windows' + +# Test 1: Successful TTY Session with Environment Variables +# ----------------------------------------------------------------------------- +# Configure TTY input stream for successful interaction test +# This test validates environment variable handling and command execution +ttyin -stdin tty.echo + +# Execute TTY test action with echo input stream +# This command runs the test-tty action with the prepared input +exec launchr test-tty + +# Validate command prompt output format +# Expected format: "/host $ " followed by environment variable assignment +stdout '^\/host \$.*TEST_VAR=bar\s*$' + +# Validate echo command display with variable substitution +# Expected format: "/host $ echo "foo ${TEST_VAR}"" +stdout '^\/host \$ echo "foo \$\{TEST_VAR\}"\s*$' + +# Validate command execution result with variable expansion +# Expected output: "foo bar" (TEST_VAR expanded to "bar") +stdout '^foo bar\s*$' + +# Validate clean execution (no error output) +! stderr . + +# Test 2: TTY Session with Error Handling and Exit Codes +# ----------------------------------------------------------------------------- +# Configure TTY input stream for error condition test +# This test validates error output handling and non-zero exit codes +ttyin -stdin tty.exit + +# Execute TTY test action expecting failure (non-zero exit code) +# This command should fail due to exit code 42 in the input stream +! exec launchr test-tty + +# Validate stderr output is captured and displayed +# Expected output: "output to stderr" from the container command +stdout '^output to stderr\s*$' + +# Validate exit code reporting in action completion message +# Expected format: action "test-tty" finished with exit code 42 +stdout 'action "test-tty" finished with exit code 42' + +# Validate clean error handling (no stderr output from launchr itself) +! stderr . + +# Test Configuration Files +# ============================================================================= + +# Action Definition: TTY Test Action Configuration +# ----------------------------------------------------------------------------- +# This file defines the containerized action for TTY testing +# Uses Alpine Linux container with shell command execution +-- actions/test-tty/action.yaml -- +action: + title: remote + description: Test tty + +runtime: + type: container + image: alpine:latest + command: + - /bin/sh + +# TTY Input Stream: Successful Echo Test +# ----------------------------------------------------------------------------- +# Input sequence for successful TTY interaction test +# Sets environment variable, executes echo command, and exits cleanly +-- ./tty.echo -- +TEST_VAR=bar +echo "foo ${TEST_VAR}" +exit + +# TTY Input Stream: Error Condition Test +# ----------------------------------------------------------------------------- +# Input sequence for error handling test +# Outputs to stderr and exits with non-zero code (42) +-- ./tty.exit -- +echo "output to stderr" >&2 +exit 42 + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# TTY Integration Rules: +# 1. Process interactive terminal input streams correctly +# 2. Handle environment variable assignment and expansion +# 3. Capture and display both stdout and stderr output +# 4. Report exit codes accurately in completion messages +# 5. Maintain clean error handling without launchr stderr output +# +# Terminal Output Format: +# - Command prompts: "/host $ " prefix format +# - Environment variables: Proper expansion and display +# - Error output: Captured from container stderr +# - Exit codes: Reported in completion messages +# +# Platform Support: +# - Full support on Unix-like systems (Linux, macOS) +# - Not supported on Windows platforms +# - Graceful skip behavior for unsupported platforms +# +# Container Runtime: +# - Uses Alpine Linux base image for consistency +# - Executes commands via /bin/sh shell +# - Proper isolation and resource management +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/runtime/shell/basic.txtar b/test/testdata/runtime/shell/basic.txtar new file mode 100644 index 0000000..f50bd30 --- /dev/null +++ b/test/testdata/runtime/shell/basic.txtar @@ -0,0 +1,266 @@ +# ============================================================================= +# Launchr Shell Runtime Environment and Action Execution Test +# ============================================================================= +# +# This test validates the Launchr tool's core shell runtime functionality: +# 1. Environment variable handling and propagation +# 2. Cross-platform compatibility (Windows/Unix) +# 3. Action execution with custom environments +# 4. Binary path resolution and template variables +# 5. Self-calling action capabilities +# 6. Standard output and error stream handling +# +# Test Focus: +# - Shell runtime environment configuration +# - Environment variable inheritance and overrides +# - Cross-platform path handling +# - Action directory and binary path resolution +# - Nested action execution +# - Stream output validation +# ============================================================================= + +# Windows WSL Environment Configuration +# ----------------------------------------------------------------------------- +# Configure WSL environment variables for Windows compatibility +# WSLENV allows environment variables to be passed between Windows and WSL +[windows] env WSLENV="HOST_ENV_1:HOST_ENV_2:$WSLENV" + +# Host Environment Setup +# ----------------------------------------------------------------------------- +# Set up host-level environment variables that will be inherited by actions +env HOST_ENV_1=foo +env HOST_ENV_2=bar + +# Primary Action Execution Test +# ----------------------------------------------------------------------------- +# Execute the environment test action with verbose logging +# Tests environment variable handling and path resolution +exec launchr test-shell:env -vvvv + +# Environment Variable Validation +# ----------------------------------------------------------------------------- +# Verify that action-level environment variables are properly set +# ACTION_ENV_1 should inherit from HOST_ENV_1, ACTION_ENV_2 should be overridden +stdout '^ACTION_ENV_1=foo ACTION_ENV_2=buz$' + +# Verify that host environment variables are accessible and can be overridden +# HOST_ENV_1 should remain unchanged, HOST_ENV_2 should be overridden to "fred" +stdout '^HOST_ENV_1=foo HOST_ENV_2=fred$' + +# Verify template variable resolution in shell context +# Shows how environment variables are expanded in shell scripts +stdout '^host: foo bar$' + +# Cross-Platform Path Resolution Tests +# ----------------------------------------------------------------------------- +# Unix-specific path validation - template and shell variable expansion +[unix] stdout '^action dir tpl: '$WORK'/test-shell/actions/env$' +[unix] stdout '^action dir sh: '$WORK'/test-shell/actions/env$' + +# Windows-specific path validation - handles backslash separators and drive letters +[windows] stdout '^action dir tpl: '${WORK@R}'\\test-shell\\actions\\env$' +[windows] stdout '^action dir sh: '$WORK_UNIX'/test-shell/actions/env$' + +# Binary Path Resolution Tests +# ----------------------------------------------------------------------------- +# Unix binary path validation - template and shell contexts +[unix] stdout '^current bin tpl: /.*/launchr$' +[unix] stdout '^current bin sh: /.*/launchr$' + +# Windows binary path validation - .exe extension and path formats +[windows] stdout '^current bin tpl: C:\\.*\\launchr\.exe$' +[windows] stdout '^current bin sh: /.*/launchr\.exe$' + +# Error Stream Validation +# ----------------------------------------------------------------------------- +# Ensure no unexpected errors are written to stderr during normal execution +! stderr . + +# Standard Error Output Test +# ----------------------------------------------------------------------------- +# Execute action that specifically tests stderr output handling +exec launchr test-shell:stderr + +# Verify proper stream separation - stdout and stderr should be distinct +stdout '^output to stdout$' +stderr '^output to stderr$' + +# Self-Calling Action Test +# ----------------------------------------------------------------------------- +# Execute action that calls the launchr binary recursively +# Tests nested execution and environment inheritance +exec launchr test-shell:call-self + +# Version Information Validation +# ----------------------------------------------------------------------------- +# Verify that self-called binary reports correct version +stdout '^launchr version v0\.0\.0-testscript' + +# Nested Environment Variable Tests +# ----------------------------------------------------------------------------- +# Verify environment handling in nested action calls +# HOST_ENV_1 should be overridden to "buz" in nested context +stdout '^ACTION_ENV_1=buz ACTION_ENV_2=buz$' +stdout '^HOST_ENV_1=buz HOST_ENV_2=fred$' + +# Verify template expansion in nested calls +stdout '^host: buz waldo$' + +# Nested Path Resolution Tests +# ----------------------------------------------------------------------------- +# Unix nested path validation +[unix] stdout '^action dir tpl: '$WORK'/test-shell/actions/env$' +[unix] stdout '^action dir sh: '$WORK'/test-shell/actions/env$' + +# Windows nested path validation +[windows] stdout '^action dir tpl: '${WORK@R}'\\test-shell\\actions\\env$' +[windows] stdout '^action dir sh: '$WORK_UNIX'/test-shell/actions/env$' + +# Nested binary path validation +[unix] stdout '^current bin tpl: /.*/launchr$' +[unix] stdout '^current bin sh: /.*/launchr$' +[windows] stdout '^current bin tpl: C:\\.*\\launchr\.exe$' +[windows] stdout '^current bin sh: /.*/launchr\.exe$' + +# Parent Action Context Tests +# ----------------------------------------------------------------------------- +# Verify that parent action directory is properly tracked in nested calls +[unix] stdout '^parent action dir tpl: '$WORK'/test-shell/actions/call-self$' +[unix] stdout '^parent action dir sh: '$WORK'/test-shell/actions/call-self$' +[windows] stdout '^parent action dir tpl: '${WORK@R}'\\test-shell\\actions\\call-self$' +[windows] stdout '^parent action dir sh: '$WORK_UNIX'/test-shell/actions/call-self$' + +# Nested Error Stream Validation +# ----------------------------------------------------------------------------- +# Verify stderr handling in nested action calls +stderr '^output to stderr$' + +# ============================================================================= +# Test Data Files - Shell Action Configurations +# ============================================================================= + +# Environment Variable Test Action +-- test-shell/actions/env/action.yaml -- +# Action demonstrating environment variable handling and path resolution +# Tests variable inheritance, overrides, and template expansion +action: + title: shell action - environment variables # Human-readable action name + +# Shell Runtime Configuration: +# Demonstrates environment variable configuration and script execution +runtime: + type: shell # Shell execution type + env: # Environment variable configuration + ACTION_ENV_1: ${HOST_ENV_1} # Inherit from host environment + ACTION_ENV_2: "buz" # Override with literal value + HOST_ENV_2: "fred" # Override host variable + script: | # Inline shell script + # Environment Variable Output: + # Display action-level environment variables + echo "ACTION_ENV_1=$$ACTION_ENV_1 ACTION_ENV_2=$$ACTION_ENV_2" + + # Host Variable Output: + # Display host-level environment variables (potentially overridden) + echo "HOST_ENV_1=$$HOST_ENV_1 HOST_ENV_2=$$HOST_ENV_2" + + # Template Variable Output: + # Show template expansion in shell context + echo "host: $HOST_ENV_1 $HOST_ENV_2" + + # Path Resolution Output: + # Display action directory paths in template and shell contexts + echo "action dir tpl: $ACTION_DIR" + echo "action dir sh: $$ACTION_DIR" + + # Binary Path Output: + # Display current binary paths in template and shell contexts + echo "current bin tpl: $CBIN" + echo "current bin sh: $$CBIN" + +# Self-Calling Test Action +-- test-shell/actions/call-self/action.yaml -- +# Action demonstrating recursive binary execution and environment inheritance +# Tests nested action calls and parent context tracking +action: + title: shell action - call current binary # Human-readable action name + +# Shell Runtime Configuration: +# Executes nested launchr calls with modified environment +runtime: + type: shell # Shell execution type + script: | # Inline shell script + # Version Information: + # Display version of current binary + $$CBIN --version + echo "" + + # Environment Modification: + # Set new environment variables for nested call + export HOST_ENV_1=buz HOST_ENV_2=waldo + + # Nested Action Execution: + # Call the environment test action with modified environment + $$CBIN test-shell:env + + # Parent Context Output: + # Display parent action directory information + echo "parent action dir tpl: $ACTION_DIR" + echo "parent action dir sh: $$ACTION_DIR" + + # Nested Error Stream Test: + # Execute stderr test action to verify stream handling + $$CBIN test-shell:stderr + +# Error Stream Test Action +-- test-shell/actions/stderr/action.yaml -- +# Action demonstrating standard output and error stream separation +# Tests proper handling of stdout and stderr in shell actions +action: + title: shell action - stderr # Human-readable action name + +# Shell Runtime Configuration: +# Executes script that writes to both stdout and stderr +runtime: + type: shell # Shell execution type + script: | # Inline shell script + # Standard Output: + # Write message to standard output stream + echo "output to stdout" # Write to standard output + + # Standard Error: + # Write message to standard error stream + echo "output to stderr" >&2 # Write to standard error + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Environment Variable Rules: +# 1. Host environment variables are inherited by actions +# 2. Action-level env configuration can override host variables +# 3. Template variables (${VAR}) are expanded before script execution +# 4. Shell variables ($$VAR) are expanded during script execution +# 5. Environment modifications in scripts affect nested calls +# +# Path Resolution Rules: +# 1. ACTION_DIR provides the current action's directory path +# 2. CBIN provides the path to the current launchr binary +# 3. Paths are formatted appropriately for the target platform +# 4. Template context uses platform-native separators +# 5. Shell context may use Unix-style paths even on Windows +# +# Cross-Platform Compatibility: +# 1. Windows tests include WSL environment configuration +# 2. Path separators are handled correctly (/ vs \) +# 3. Binary extensions are platform-appropriate (.exe on Windows) +# 4. Drive letters and UNC paths are supported on Windows +# 5. Unix and Windows paths coexist in mixed environments +# +# Stream Handling: +# 1. Standard output and error streams are properly separated +# 2. Actions can write to both stdout and stderr independently +# 3. Stream redirection works correctly in shell scripts +# 4. Error output from nested calls is properly propagated +# 5. No unexpected errors should appear during normal execution +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/runtime/shell/kill.txtar b/test/testdata/runtime/shell/kill.txtar new file mode 100644 index 0000000..e160d01 --- /dev/null +++ b/test/testdata/runtime/shell/kill.txtar @@ -0,0 +1,125 @@ +# ============================================================================= +# Launchr Signal Handling and Process Kill Functionality Test +# ============================================================================= +# +# This test validates the Launchr tool's ability to: +# 1. Execute long-running shell actions in the background +# 2. Handle SIGTERM signal forwarding to child processes +# 3. Properly terminate background processes with custom exit codes +# +# Test Focus: +# - Signal handling in shell runtime actions +# - Background process execution and termination +# - Custom signal trap configuration +# - Process lifecycle management +# - Exit code validation after signal termination +# ============================================================================= + +# Platform Compatibility Check +# ----------------------------------------------------------------------------- +# Skip this test on Windows as signal handling is not supported by testscript. +[windows] skip 'testing kill is not supported on windows' + +# Execute signal-handling action in background with verbose logging +# The action sets up signal traps and waits for termination signals +! exec launchr test-signal &appint& + +# Allow time for process startup and signal trap configuration +# Ensures the action is running and ready to receive signals +sleep 1 + +# Send SIGTERM signal to the background process +# Note: testscript normally doesn't support TERM signal, but we override +# this functionality in custom code (see [test.CmdKill]) +# We use TERM instead of INT because `go test` blocks SIGINT in subprocesses +# TERM is not ignored so we can test signal handling properly +kill -TERM appint + +# Wait for the background process to complete after signal +# Should exit with custom code (42) rather than normal completion +wait appint + +# Validate that signal trap was properly configured and executed +# Should show startup message indicating the process began waiting +stdout 'Waiting for signals...' + +# Validate that SIGTERM was received and handled by the trap +# Should show signal reception message from the trap handler +stdout 'Received signal: SIGTERM' + +# Validate that process exited with expected custom code +# Should show exit code 42 from signal handler, not 45 from normal completion +stdout 'finished with exit code 42' + +# ============================================================================= +# Test Data Files - Signal Handling Action Configuration +# ============================================================================= + +# Shell Action with Signal Handling +-- actions/test-signal/action.yaml -- +# Action demonstrating signal handling and trap configuration +# Tests the ability to receive and respond to termination signals +action: + title: test signal forwarding # Human-readable action name + description: Test signal forwarding # Action description + +# Shell Runtime Configuration: +# Executes shell script with signal handling capabilities +runtime: + type: shell # Shell execution type + script: | # Inline shell script + # Signal Handler Configuration: + # Define custom handler for SIGTERM signal + handle_interrupt() { + echo "Received signal: SIGTERM" # Log signal reception + exit 42 # Exit with custom code + } + + # Trap Registration: + # Register signal handler for TERM signal + trap handle_interrupt TERM + echo "Trap set: $(trap -p TERM)" # Confirm trap configuration + + # Main Process Loop: + # Wait for signals while performing background work + echo "Waiting for signals..." + for i in {1..5}; do + sleep 1 # Sleep allows signal interruption + done + + # Normal Completion Path: + # This should NOT be reached in successful signal test + echo "No signal received, exiting normally" + exit 45 # Different exit code for normal completion + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Signal Handling Rules: +# 1. Shell actions can define custom signal handlers using trap +# 2. SIGTERM signals should be properly forwarded to child processes +# 3. Signal handlers should execute and override normal completion +# 4. Custom exit codes should be preserved through signal handling +# 5. Background processes should respond to kill commands appropriately +# +# Process Lifecycle: +# - Action starts in background (&appint& syntax) +# - Process sets up signal traps and begins waiting +# - External kill command sends SIGTERM to process +# - Signal handler executes and terminates with custom code +# - Test validates proper signal reception and handling +# +# Exit Code Validation: +# - Normal completion would exit with code 45 +# - Signal handler completion exits with code 42 +# - Test verifies signal handling by checking exit code 42 +# - This confirms signal was received and handled properly +# +# Output Verification: +# - "Waiting for signals..." confirms process startup +# - "Received signal: SIGTERM" confirms signal reception +# - "finished with exit code 42" confirms proper termination +# - Normal completion message should NOT appear +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/runtime/shell/logger.txtar b/test/testdata/runtime/shell/logger.txtar new file mode 100644 index 0000000..1034624 --- /dev/null +++ b/test/testdata/runtime/shell/logger.txtar @@ -0,0 +1,107 @@ +# ============================================================================= +# Launchr Log Format and Level Propagation Test +# ============================================================================= +# +# This test validates the Launchr tool's ability to: +# 1. Propagate log format settings to subprocess actions +# 2. Propagate log level settings to subprocess actions +# 3. Handle mixed log formats within single execution +# 4. Apply quiet mode to suppress all subprocess output +# 5. Maintain consistent logging behavior across action boundaries +# +# Test Focus: +# - Log configuration inheritance by subprocess actions +# - Format propagation (plain and JSON) to nested calls +# - Level propagation and filtering in subprocess execution +# - Quiet mode effect on subprocess logging +# - Shutdown cleanup logging across action hierarchy +# ============================================================================= + +# Test Log Configuration Propagation +# ----------------------------------------------------------------------------- +# Execute action with plain format and DEBUG level +# Subprocess actions should inherit these settings and display all log levels +exec launchr --log-format=plain --log-level=DEBUG test-shell:log-levels + +# Validate subprocess inherits plain format and shows all levels +stdout '^time=.* level=DEBUG msg="this is DEBUG log"$' +stdout '^time=.* level=INFO msg="this is INFO log"$' +stdout '^time=.* level=WARN msg="this is WARN log"$' +stdout '^time=.* level=ERROR msg="this is ERROR log"$' + +# Validate subprocess can override format to JSON while maintaining level +stdout '^{"time":".+","level":"ERROR","msg":"log output: MySensitiveValue"}$' + +# Validate shutdown cleanup appears for each subprocess +stdout -count=2 '^time=.* level=DEBUG msg="shutdown cleanup"' + +# Test Quiet Mode Propagation +# ----------------------------------------------------------------------------- +# Execute action in quiet mode +# All subprocess output should be suppressed +exec launchr -q test-shell:log-levels + +# Validate that subprocess outputs are suppressed +! stdout . +! stderr . + +# ============================================================================= +# Test Data Files - Log Propagation Action Configuration +# ============================================================================= + +# Shell Action that Spawns Subprocess Actions +-- test-shell/actions/log-levels/action.yaml -- +# Action demonstrating log configuration propagation to subprocess actions +# Tests inheritance and override of log format and level settings +action: + title: shell action - log levels # Human-readable action name + +# Shell Runtime Configuration: +# Executes subprocess actions to test log configuration propagation +runtime: + type: shell # Shell execution type + script: | # Inline shell script + # Subprocess inherits parent log format and level + # Should display logs according to parent configuration + $$CBIN testplugin:log-levels + + # Subprocess overrides format to JSON but inherits level + # Tests selective override of log configuration + $$CBIN testplugin:sensitive --log-format=json --log-level=ERROR 'MySensitiveValue' + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Log Configuration Inheritance: +# 1. Subprocess actions inherit parent log format by default +# 2. Subprocess actions inherit parent log level by default +# 3. Subprocess actions can override specific log settings +# 4. Overrides affect only that subprocess, not siblings +# 5. Inheritance ensures consistent logging behavior +# +# Format Propagation: +# - Plain format propagates to subprocess unless overridden +# - JSON format can be selectively applied to specific subprocess +# - Mixed formats within single execution are supported +# - Format inheritance maintains output consistency +# +# Level Propagation: +# - DEBUG level shows all subprocess log messages +# - Level filtering applies consistently to all subprocess +# - Subprocess can override level for specific needs +# - Shutdown cleanup messages respect parent level settings +# +# Quiet Mode Propagation: +# - Quiet mode suppresses all subprocess output +# - Propagation is absolute - no subprocess output appears +# - Overrides any log format or level configuration +# - Provides complete silence across action hierarchy +# +# Subprocess Action Behavior: +# - Each subprocess maintains separate log context +# - Shutdown cleanup occurs for each subprocess +# - Multiple subprocess calls generate multiple cleanup messages +# - Log settings are inherited at subprocess spawn time +# +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/runtime/shell/sensitive.txtar b/test/testdata/runtime/shell/sensitive.txtar new file mode 100644 index 0000000..1c80742 --- /dev/null +++ b/test/testdata/runtime/shell/sensitive.txtar @@ -0,0 +1,121 @@ +# ============================================================================= +# Launchr Sensitive Data Handling and Output Filtering Test +# ============================================================================= +# +# This test validates the Launchr tool's ability to: +# 1. Handle sensitive data in shell action outputs +# 2. Filter sensitive values from stdout and stderr streams +# 3. Handle split sensitive strings across multiple echo commands +# 4. Process sensitive data through nested action calls +# +# Test Focus: +# - Sensitive data detection and redaction +# - Cross-stream filtering (stdout and stderr) +# - Nested action execution with sensitive data +# ============================================================================= + +# Windows WSL Environment Configuration +# ----------------------------------------------------------------------------- +# Configure WSL environment variables for Windows compatibility +# WSLENV allows environment variables to be passed between Windows and WSL +[windows] env WSLENV="TEST_SECRET:$WSLENV" + +# Test Normal Mode Execution +# ----------------------------------------------------------------------------- +# Execute sensitive action in normal mode (no filtering) +# Should display all sensitive values as-is without redaction +exec launchr test-shell:sensitive + +# Validate unfiltered output contains actual sensitive values +stdout '^subshell: MySuper SecretValue$' +stdout '^subshell split: MySuper SecretValue$' +stdout '^terminal output: MySuper SecretValue$' +stdout 'log output: MySuper SecretValue\s+' +stdout '^terminal output: OtherSecret$' +stdout 'log output: OtherSecret\s+' +stderr '^subshell stderr: MySuper SecretValue$' + +# Test Environment Variable Based Filtering +# ----------------------------------------------------------------------------- +# Set environment variable to enable sensitive value detection +# When TEST_SECRET is set, matching values should be redacted as **** +env TEST_SECRET='MySuper SecretValue' +exec launchr test-shell:sensitive + +# Validate that sensitive values are redacted when environment variable is set +stdout '^subshell: \*\*\*\*$' +stdout '^subshell split: \*\*\*\*$' +stdout -count=2 '^terminal output: \*\*\*\*$' # We check twice for the 2nd "OtherSecret" +stdout -count=2 'log output: \*\*\*\*\s+' +stderr '^subshell stderr: \*\*\*\*$' +stderr -count=2 '^fmt stderr streams print: \*\*\*\*$' + +# ============================================================================= +# Test Data Files - Sensitive Data Action Configuration +# ============================================================================= + +# Shell Action with Sensitive Data Output +-- test-shell/actions/sensitive/action.yaml -- +# Action demonstrating sensitive data handling across multiple output streams +# Tests various scenarios of sensitive value detection and filtering +action: + title: shell action - sensitive # Human-readable action name + +# Shell Runtime Configuration: +# Executes shell script that outputs sensitive data in multiple ways +runtime: + type: shell # Shell execution type + script: | # Inline shell script + # Direct sensitive output to stdout + echo 'subshell: MySuper SecretValue' + + # Direct sensitive output to stderr + echo 'subshell stderr: MySuper SecretValue' >&2 + + # Split sensitive string across multiple echo commands + # Tests detection of sensitive values assembled from parts + echo -n 'subshell split: MySuper' + echo -n ' ' + echo 'SecretValue' + + # Environment variable based sensitive value switching + # If TEST_SECRET is set, use different sensitive value + [[ -n "$$TEST_SECRET" ]] && export TEST_SECRET=OtherSecret + + # Nested action calls with sensitive data + # Tests sensitive handling through action composition + $$CBIN testplugin:sensitive -v 'MySuper SecretValue' + $$CBIN testplugin:sensitive -v 'OtherSecret' + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Sensitive Data Detection Rules: +# 1. Normal mode displays all sensitive values without filtering +# 2. Sensitive values are redacted as **** when detection is enabled +# 3. Detection works across stdout and stderr streams +# 4. Split sensitive strings are properly detected and handled +# +# Output Filtering Modes: +# - Environment-based filtering: Sensitive values redacted as **** +# - Cross-stream filtering: Both stdout and stderr are processed +# +# Sensitive Value Detection: +# - Direct string matching for known sensitive values +# - Multi-part string assembly detection +# - Nested action output filtering +# - Both exact matches and partial matches are handled +# +# Stream Handling: +# - stdout: Regular output stream processing +# - stderr: Error stream processing with same filtering rules +# - Log output: Formatted log messages with sensitive data +# +# Action Composition: +# - Nested action calls ($$CBIN testplugin:sensitive) +# - Sensitive data passed through action parameters +# - Consistent filtering across action boundaries +# - Proper handling of composed sensitive operations +# +# ============================================================================= \ No newline at end of file diff --git a/test/testscript.dlv.go b/test/testscript.dlv.go new file mode 100644 index 0000000..2879f7d --- /dev/null +++ b/test/testscript.dlv.go @@ -0,0 +1,90 @@ +package test + +import ( + "net" + "path/filepath" + "strconv" + _ "unsafe" // Include an internal method of the testscript module. + + "github.com/rogpeppe/go-internal/testscript" + + "github.com/launchrctl/launchr/internal/launchr" +) + +// CmdDlv implements a custom testscript command for debugging with Delve +func CmdDlv(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("dlv command does not support negation") + } + + if len(args) < 1 { + ts.Fatalf("dlv: missing binary name\nUsage: dlv [args...]") + } + + // Check if running in debug mode + if !launchr.Version().Debug { + ts.Fatalf("dlv command requires the tests to be run with debug flags") + } + + command := args[0] + binaryArgs := args[1:] + if filepath.Base(command) == command { + if lp, err := lookPath(command, ts.Getenv); err != nil { + ts.Fatalf("error when looking for %s: %v", command, err) + } else { + command = lp + } + } + + // Find an available port + port := findAvailablePort() + + // Log connection information + ts.Logf("=== Delve Debug Server ===") + ts.Logf("Debugging binary: %s", command) + ts.Logf("Port: %d", port) + ts.Logf("Connect with: dlv connect 127.0.0.1:%d", port) + ts.Logf("GoLand Remote Debug: 127.0.0.1:%d", port) + ts.Logf("=========================") + + // Build dlv command arguments + cmdArgs := []string{ + "exec", command, + "--listen=127.0.0.1:" + strconv.Itoa(port), + "--headless=true", + "--api-version=2", + "--accept-multiclient", + } + + // Add binary arguments if any + if len(binaryArgs) > 0 { + cmdArgs = append(cmdArgs, "--") + cmdArgs = append(cmdArgs, binaryArgs...) + } + + // Execute dlv using testscript's exec method + _ = ts.Exec("dlv", cmdArgs...) +} + +//go:linkname lookPath github.com/rogpeppe/go-internal/internal/os/execpath.Look +func lookPath(file string, getenv func(string) string) (string, error) + +// findAvailablePort finds an available port starting from 2345 +func findAvailablePort() int { + for port := 2345; port <= 2355; port++ { + if isPortAvailable(port) { + return port + } + } + return 2345 // fallback +} + +// isPortAvailable checks if a port is available +func isPortAvailable(port int) bool { + ln, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port)) + if err != nil { + return false + } + _ = ln.Close() + return true +} diff --git a/test/testscript.go b/test/testscript.go new file mode 100644 index 0000000..2b5ef35 --- /dev/null +++ b/test/testscript.go @@ -0,0 +1,138 @@ +// Package test contains functionality to test the application with testscript. +package test + +import ( + "os" + "os/exec" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/rogpeppe/go-internal/testscript" + + "github.com/launchrctl/launchr/internal/launchr" + _ "github.com/launchrctl/launchr/test/plugins" // Include test plugins. +) + +// CmdsTestScript provides custom commands for testscript execution. +func CmdsTestScript() map[string]func(ts *testscript.TestScript, neg bool, args []string) { + return map[string]func(ts *testscript.TestScript, neg bool, args []string){ + // txtproc provides flexible text processing capabilities + // Usage: + // txtproc replace 'old' 'new' input.txt output.txt + // txtproc replace-regex 'pattern' 'replacement' input.txt output.txt + // txtproc remove-lines 'pattern' input.txt output.txt + // txtproc remove-regex 'pattern' input.txt output.txt + // txtproc extract-lines 'pattern' input.txt output.txt + // txtproc extract-regex 'pattern' input.txt output.txt + "txtproc": CmdTxtProc, + // sleep pauses execution for a specified duration + // Usage: + // sleep + // Examples: + // sleep 1s + // sleep 500ms + // sleep 2m + "sleep": CmdSleep, + // dlv runs the given binary with Delve for debugging. + // Please, note that the test must be run with debug headers for it to work. + // Usage: + // dlv + "dlv": CmdDlv, + } +} + +// SetupEnvDocker configures docker backend in the test environment. +func SetupEnvDocker(env *testscript.Env) error { + env.Vars = append( + env.Vars, + // Passthrough Docker env variables if set. + "DOCKER_HOST="+os.Getenv("DOCKER_HOST"), + "DOCKER_TLS_VERIFY="+os.Getenv("DOCKER_TLS_VERIFY"), + "DOCKER_CERT_PATH="+os.Getenv("DOCKER_CERT_PATH"), + ) + return nil +} + +// SetupEnvRandom sets up a random environment variable. +func SetupEnvRandom(env *testscript.Env) error { + env.Vars = append( + env.Vars, + "RANDOM="+launchr.GetRandomString(8), + ) + return nil +} + +// SetupWorkDirUnixWin sets up a work dir env variable in unix style. +func SetupWorkDirUnixWin(env *testscript.Env) error { + env.Vars = append( + env.Vars, + "WORK_UNIX="+launchr.ConvertWindowsPath(env.WorkDir), + ) + return nil +} + +// SetupWSL sets up a work dir env variable using WSL mount path. +func SetupWSL(t *testing.T) func(env *testscript.Env) error { + return func(env *testscript.Env) error { + // Take WSL script path from env variable. + wslBashPath := os.Getenv("TEST_WSL_BASH_PATH") + // Try to create a wrapper for WSL. + if wslBashPath == "" { + wslpath, err := exec.LookPath("wsl") + if err != nil { + panic(err) + } + content := "@echo off\r\n" + wslpath + " bash %*" + + // Create the file + wslBashPath = filepath.Join(t.TempDir(), "wsl-bash.cmd") + file, err := os.Create(wslBashPath) //nolint:gosec // G304 We create the path. + if err != nil { + panic(err) + } + + // Write the content to the file + _, err = file.WriteString(content) + if err != nil { + _ = file.Close() + panic(err) + } + _ = file.Close() + } + env.Vars = append( + env.Vars, + "WORK_UNIX=/mnt"+launchr.ConvertWindowsPath(env.WorkDir), + "LAUNCHR_RUNTIME_SHELL_BASH="+wslBashPath, + ) + return nil + } +} + +// CmdSleep pauses execution for a specified duration +func CmdSleep(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("sleep does not support negation") + } + + if len(args) != 1 { + ts.Fatalf("sleep: usage: sleep ") + } + + duration, err := time.ParseDuration(args[0]) + if err != nil { + // Try parsing as seconds if it's just a number + if seconds, numErr := strconv.ParseFloat(args[0], 64); numErr == nil { + duration = time.Duration(seconds * float64(time.Second)) + } else { + ts.Fatalf("sleep: invalid duration %q: %v", args[0], err) + } + } + + if duration < 0 { + ts.Fatalf("sleep: duration cannot be negative") + } + + time.Sleep(duration) +} diff --git a/test/testscript.kill.go b/test/testscript.kill.go new file mode 100644 index 0000000..405ec1d --- /dev/null +++ b/test/testscript.kill.go @@ -0,0 +1,80 @@ +//go:build unix || windows + +package test + +import ( + "os" + "strings" + "syscall" + _ "unsafe" // Include an internal method of the testscript module. + + "github.com/rogpeppe/go-internal/testscript" +) + +func init() { + tsScriptCmds["kill"] = CmdKill +} + +var supportedKillSignals = map[string]syscall.Signal{ + "HUP": syscall.SIGHUP, + "INT": syscall.SIGINT, + "QUIT": syscall.SIGQUIT, + "ILL": syscall.SIGILL, + "TRAP": syscall.SIGTRAP, + "ABRT": syscall.SIGABRT, + "BUS": syscall.SIGBUS, + "FPE": syscall.SIGFPE, + "KILL": syscall.SIGKILL, + "SEGV": syscall.SIGSEGV, + "PIPE": syscall.SIGPIPE, + "ALRM": syscall.SIGALRM, + "TERM": syscall.SIGTERM, +} + +// CmdKill is an override of [github.com/rogpeppe/go-internal/testscript.(*TestScript).cmdKill]. +// It supports more kill signals than the original. +func CmdKill(ts *testscript.TestScript, neg bool, args []string) { + var ( + name string + signal os.Signal + ) + switch len(args) { + case 0: + case 1, 2: + sig, ok := strings.CutPrefix(args[0], "-") + if ok { + signal, ok = supportedKillSignals[sig] + if !ok { + ts.Fatalf("unknown signal: %s", sig) + } + } else { + name = args[0] + break + } + if len(args) == 2 { + name = args[1] + } + default: + ts.Fatalf("usage: kill [-SIGNAL] [name]") + } + if neg { + ts.Fatalf("unsupported: ! kill") + } + if signal == nil { + signal = os.Kill + } + if name != "" { + killBackgroundOne(ts, name, signal) + } else { + killBackground(ts, signal) + } +} + +//go:linkname tsScriptCmds github.com/rogpeppe/go-internal/testscript.scriptCmds +var tsScriptCmds map[string]func(*testscript.TestScript, bool, []string) + +//go:linkname killBackgroundOne github.com/rogpeppe/go-internal/testscript.(*TestScript).killBackgroundOne +func killBackgroundOne(ts *testscript.TestScript, bgName string, signal os.Signal) + +//go:linkname killBackground github.com/rogpeppe/go-internal/testscript.(*TestScript).killBackground +func killBackground(ts *testscript.TestScript, signal os.Signal) diff --git a/test/testscript.txtproc.go b/test/testscript.txtproc.go new file mode 100644 index 0000000..0199b93 --- /dev/null +++ b/test/testscript.txtproc.go @@ -0,0 +1,175 @@ +package test + +import ( + "fmt" + "os" + "regexp" + "strings" + + "github.com/rogpeppe/go-internal/testscript" +) + +// Constants for repeated string values +const ( + opReplace = "replace" + opReplaceRegex = "replace-regex" + opRemoveLines = "remove-lines" + opRemoveRegex = "remove-regex" + opExtractLines = "extract-lines" + opExtractRegex = "extract-regex" +) + +// CmdTxtProc provides flexible text processing capabilities +func CmdTxtProc(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("txtproc does not support negation") + } + + if len(args) < 3 { + ts.Fatalf("txtproc: usage: txtproc [args...] ") + } + + operation := args[0] + var inputFile, outputFile string + var pattern, replacement string + + switch operation { + case opReplace: + if len(args) != 5 { + ts.Fatalf("txtproc replace: usage: txtproc replace ") + } + pattern = args[1] + replacement = args[2] + inputFile = args[3] + outputFile = args[4] + + case opReplaceRegex: + if len(args) != 5 { + ts.Fatalf("txtproc replace-regex: usage: txtproc replace-regex ") + } + pattern = args[1] + replacement = args[2] + inputFile = args[3] + outputFile = args[4] + + case opRemoveLines, opRemoveRegex, opExtractLines, opExtractRegex: + if len(args) != 4 { + ts.Fatalf("txtproc %s: usage: txtproc %s ", operation, operation) + } + pattern = args[1] + inputFile = args[2] + outputFile = args[3] + + default: + ts.Fatalf("txtproc: unknown operation %q. Available: replace, replace-regex, remove-lines, remove-regex, extract-lines, extract-regex", operation) + } + + // Read input content + var content string + var err error + + switch inputFile { + case "stdout": + // Special case: read from testscript's stdout buffer + content = ts.Getenv("stdout") + if content == "" { + // Try to read stdout content using testscript's internal mechanism + // This is a workaround since testscript doesn't expose stdout directly + ts.Fatalf("txtproc: no stdout content available. Make sure to run 'exec' command before using txtproc with stdout") + } + case "stderr": + // Special case: read from testscript's stderr buffer + content = ts.Getenv("stderr") + if content == "" { + ts.Fatalf("txtproc: no stderr content available. Make sure to run 'exec' command before using txtproc with stderr") + } + default: + // Regular file + inputPath := ts.MkAbs(inputFile) + // #nosec G304 - File path is validated by testscript framework + contentBytes, readErr := os.ReadFile(inputPath) + if readErr != nil { + ts.Fatalf("txtproc: failed to read %s: %v", inputFile, readErr) + } + content = string(contentBytes) + } + + // Process content + result, err := processText(content, operation, pattern, replacement) + if err != nil { + ts.Fatalf("txtproc: %v", err) + } + + // Write output file + outputPath := ts.MkAbs(outputFile) + // Use more restrictive file permissions for security + err = os.WriteFile(outputPath, []byte(result), 0600) + if err != nil { + ts.Fatalf("txtproc: failed to write %s: %v", outputFile, err) + } +} + +func processText(content, operation, pattern, replacement string) (string, error) { + switch operation { + case opReplace: + return strings.ReplaceAll(content, pattern, replacement), nil + + case opReplaceRegex: + re, err := regexp.Compile("(?m)" + pattern) + if err != nil { + return "", fmt.Errorf("invalid regex %q: %v", pattern, err) + } + return re.ReplaceAllString(content, replacement), nil + + case opRemoveLines: + lines := strings.Split(content, "\n") + var result []string + for _, line := range lines { + if !strings.Contains(line, pattern) { + result = append(result, line) + } + } + return strings.Join(result, "\n"), nil + + case opRemoveRegex: + re, err := regexp.Compile(pattern) + if err != nil { + return "", fmt.Errorf("invalid regex %q: %v", pattern, err) + } + lines := strings.Split(content, "\n") + var result []string + for _, line := range lines { + if !re.MatchString(line) { + result = append(result, line) + } + } + return strings.Join(result, "\n"), nil + + case opExtractLines: + lines := strings.Split(content, "\n") + var result []string + for _, line := range lines { + if strings.Contains(line, pattern) { + result = append(result, line) + } + } + return strings.Join(result, "\n"), nil + + case opExtractRegex: + re, err := regexp.Compile(pattern) + if err != nil { + return "", fmt.Errorf("invalid regex %q: %v", pattern, err) + } + lines := strings.Split(content, "\n") + var result []string + for _, line := range lines { + if re.MatchString(line) { + result = append(result, line) + } + } + return strings.Join(result, "\n"), nil + + default: + return "", fmt.Errorf("unknown operation %q", operation) + } +}