// Copyright 2025 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package local import ( "os" "runtime" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenCmdByShell(t *testing.T) { tmpDir := t.TempDir() e := local{tempDir: tmpDir} t.Run("error cases", func(t *testing.T) { args, err := e.genCmdByShell("", []string{"echo hi"}) assert.Nil(t, args) assert.ErrorIs(t, err, ErrNoShellSet) args, err = e.genCmdByShell("sh", []string{}) assert.Nil(t, args) assert.ErrorIs(t, err, ErrNoCmdSet) }) t.Run("windows shells", func(t *testing.T) { t.Run("cmd", func(t *testing.T) { args, err := e.genCmdByShell("cmd.exe", []string{"echo hi", "call build.bat"}) require.NoError(t, err) require.Len(t, args, 2) assert.Equal(t, "/c", args[0]) assert.True(t, strings.HasSuffix(args[1], ".cmd")) // Verify the temp file was created and contains expected content content, err := os.ReadFile(args[1]) require.NoError(t, err) assert.EqualValues(t, `@SET PROMPT=$ @echo + 'echo hi' @echo hi @IF NOT %ERRORLEVEL% == 0 exit %ERRORLEVEL% @echo + 'call build.bat' @call build.bat @IF NOT %ERRORLEVEL% == 0 exit %ERRORLEVEL% `, string(content)) }) t.Run("powershell", func(t *testing.T) { args, err := e.genCmdByShell("powershell", []string{"Write-Host 'test'", "echo test"}) require.NoError(t, err) require.Len(t, args, 4) assert.EqualValues(t, []string{"-noprofile", "-noninteractive", "-c"}, []string{args[0], args[1], args[2]}) assert.EqualValues(t, `$ErrorActionPreference = "Stop"; echo '+ Write-Host '"'"'test'"'"'' Write-Host 'test' echo '+ echo test' echo test`, args[3]) args, err = e.genCmdByShell("pwsh", []string{"Get-Process"}) require.NoError(t, err) assert.Len(t, args, 4) assert.Equal(t, "-noprofile", args[0]) }) }) t.Run("unix shells", func(t *testing.T) { args, err := e.genCmdByShell("sh", []string{"echo hello", "pwd"}) require.NoError(t, err) assert.Len(t, args, 3) assert.Equal(t, "-e", args[0]) assert.Equal(t, "-c", args[1]) assert.Contains(t, args[2], "echo hello") assert.Contains(t, args[2], "pwd") args, err = e.genCmdByShell("bash", []string{"ls -la"}) require.NoError(t, err) assert.Len(t, args, 3) assert.Equal(t, "-e", args[0]) assert.Equal(t, "-c", args[1]) args, err = e.genCmdByShell("zsh", []string{"echo test"}) require.NoError(t, err) assert.Len(t, args, 3) assert.Equal(t, "-e", args[0]) }) t.Run("fish shell", func(t *testing.T) { args, err := e.genCmdByShell("fish", []string{"echo test", "ls"}) require.NoError(t, err) assert.Len(t, args, 2) assert.Equal(t, "-c", args[0]) assert.Contains(t, args[1], "echo test") assert.Contains(t, args[1], "|| exit $status") }) t.Run("nu shell", func(t *testing.T) { args, err := e.genCmdByShell("nu", []string{"echo test"}) require.NoError(t, err) assert.Len(t, args, 2) assert.Equal(t, "--commands", args[0]) assert.Contains(t, args[1], "echo test") }) t.Run("command escaping", func(t *testing.T) { args, err := e.genCmdByShell("cmd", []string{"echo 'test with | pipe'", "echo 'test & ampersand'\n\necho new line"}) require.NoError(t, err) content, err := os.ReadFile(args[1]) require.NoError(t, err) assert.EqualValues(t, `@SET PROMPT=$ @echo + 'echo '"'"'test with \OR pipe'"'"'' @echo 'test with | pipe' @IF NOT %ERRORLEVEL% == 0 exit %ERRORLEVEL% @echo + 'echo '"'"'test \AND ampersand'"'"'\n\necho new line' @echo 'test & ampersand' echo new line @IF NOT %ERRORLEVEL% == 0 exit %ERRORLEVEL% `, string(content)) }) t.Run("shell with .exe suffix", func(t *testing.T) { args, err := e.genCmdByShell("bash.exe", []string{"echo test"}) require.NoError(t, err) assert.Len(t, args, 3) assert.Equal(t, "-e", args[0]) }) } func TestProbeShellIsPosix(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("skipping posix shell tests on non-linux system") } t.Run("valid posix shells", func(t *testing.T) { err := probeShellIsPosix("sh") assert.NoError(t, err) }) t.Run("invalid shell", func(t *testing.T) { err := probeShellIsPosix("nonexistentshell12345") if assert.ErrorIs(t, err, &ErrNoPosixShell{}) { assert.Equal(t, `Shell "nonexistentshell12345" was assumed to be a Posix shell, but test failed: exec: "nonexistentshell12345": executable file not found in $PATH (if you want support for it, please open an issue)`, err.Error()) } }) t.Run("non-posix shell", func(t *testing.T) { // nologin won't understand posix syntax err := probeShellIsPosix("true") if assert.ErrorIs(t, err, &ErrNoPosixShell{}) { assert.Equal(t, `Shell "true" was assumed to be a Posix shell, but test failed: unexpected output returned: "" (if you want support for it, please open an issue)`, err.Error()) } }) }