master
go 342 lines 9.15 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package ndexec
4
5 import (
6 "context"
7 "os"
8 "path/filepath"
9 "runtime"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16 )
17
18 func TestRunner_run(t *testing.T) {
19 if runtime.GOOS == "windows" {
20 t.Skip("uses sh scripts")
21 }
22
23 tmp := t.TempDir()
24
25 writeExe := func(path, body string) {
26 require.NoError(t, os.WriteFile(path, []byte(body), 0o755), "write %s", path)
27 }
28
29 // Target scripts the helper will exec into.
30 echoArgs := filepath.Join(tmp, "echoargs.sh")
31 writeExe(echoArgs, `#!/bin/sh
32 printf '%s|' "$@"
33 echo
34 `)
35
36 longErr := filepath.Join(tmp, "longerr.sh")
37 long := strings.Repeat("x", 9000) // > stderrLimit
38 writeExe(longErr, `#!/bin/sh
39 printf '`+long+`' 1>&2
40 exit 17
41 `)
42
43 sleeper := filepath.Join(tmp, "sleep.sh")
44 writeExe(sleeper, `#!/bin/sh
45 sleep "$1"
46 `)
47
48 // Fake helper (acts like nd-run/ndsudo): replaces itself with the target.
49 helper := filepath.Join(tmp, "helper.sh")
50 writeExe(helper, `#!/bin/sh
51 exec "$@"
52 `)
53
54 type tc struct {
55 helperPath string
56 argv []string
57 timeout time.Duration
58 wantOut string
59 wantErr bool
60 errContains []string
61 check func(t *testing.T, out []byte, err error)
62 }
63
64 tests := map[string]tc{
65 "success_echo_args": {
66 helperPath: helper,
67 argv: []string{echoArgs, `a b`, `c"d`},
68 timeout: 15 * time.Second,
69 wantOut: "a b|c\"d|\n",
70 },
71 "nonzero_with_trimmed_stderr": {
72 helperPath: helper,
73 argv: []string{longErr},
74 timeout: 5 * time.Second,
75 wantErr: true,
76 errContains: []string{"stderr:", "truncated"},
77 },
78 "timeout": {
79 helperPath: helper,
80 argv: []string{sleeper, "2"},
81 timeout: 200 * time.Millisecond,
82 wantErr: true,
83 errContains: []string{"deadline"},
84 check: func(t *testing.T, _ []byte, err error) {
85 // Either errors.Is(err, context.DeadlineExceeded) or message contains it.
86 assert.ErrorIs(t, err, context.DeadlineExceeded)
87 },
88 },
89 "helper_missing": {
90 helperPath: filepath.Join(tmp, "missing", "helper.sh"),
91 argv: []string{echoArgs},
92 timeout: time.Second,
93 wantErr: true,
94 errContains: []string{"no such file", "helper.sh"},
95 },
96 }
97
98 r := &runner{} // we pass helperPath directly to run()
99
100 for name, tt := range tests {
101 t.Run(name, func(t *testing.T) {
102 out, _, _, err := r.run(nil, tt.timeout, "", tt.helperPath, "RunTest", nil, tt.argv...)
103
104 if tt.wantErr {
105 require.Error(t, err)
106 for _, frag := range tt.errContains {
107 assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(frag))
108 }
109 } else {
110 require.NoError(t, err)
111 if tt.wantOut != "" {
112 assert.Equal(t, tt.wantOut, string(out))
113 }
114 }
115
116 if tt.check != nil {
117 tt.check(t, out, err)
118 }
119 })
120 }
121 }
122
123 func TestRunDirect(t *testing.T) {
124 if runtime.GOOS == "windows" {
125 t.Skip("uses sh scripts")
126 }
127
128 tmp := t.TempDir()
129
130 writeExe := func(path, body string) {
131 require.NoError(t, os.WriteFile(path, []byte(body), 0o755))
132 }
133
134 echoArgs := filepath.Join(tmp, "echoargs.sh")
135 writeExe(echoArgs, "#!/bin/sh\nprintf '%s|' \"$@\"\necho\n")
136
137 stderrScript := filepath.Join(tmp, "stderr.sh")
138 writeExe(stderrScript, "#!/bin/sh\necho 'some error' 1>&2\nexit 1\n")
139
140 sleeper := filepath.Join(tmp, "sleep.sh")
141 writeExe(sleeper, "#!/bin/sh\nsleep 2\n")
142
143 t.Run("success", func(t *testing.T) {
144 out, err := RunDirect(nil, time.Second, echoArgs, "hello", "world")
145 require.NoError(t, err)
146 assert.Equal(t, "hello|world|\n", string(out))
147 })
148
149 t.Run("non-zero exit with stderr", func(t *testing.T) {
150 _, err := RunDirect(nil, time.Second, stderrScript)
151 require.Error(t, err)
152 assert.Contains(t, err.Error(), "execution failed")
153 assert.Contains(t, err.Error(), "some error")
154 })
155
156 t.Run("timeout", func(t *testing.T) {
157 _, err := RunDirect(nil, 200*time.Millisecond, sleeper)
158 require.Error(t, err)
159 assert.Contains(t, err.Error(), "execution failed")
160 })
161
162 t.Run("binary not found", func(t *testing.T) {
163 _, err := RunDirect(nil, time.Second, filepath.Join(tmp, "nonexistent"))
164 require.Error(t, err)
165 assert.Contains(t, err.Error(), "execution failed")
166 })
167
168 t.Run("long stderr truncated", func(t *testing.T) {
169 longStderr := filepath.Join(tmp, "longstderr.sh")
170 writeExe(longStderr, "#!/bin/sh\nprintf '"+strings.Repeat("x", 9000)+"' 1>&2\nexit 1\n")
171
172 _, err := RunDirect(nil, 5*time.Second, longStderr)
173 require.Error(t, err)
174 assert.Contains(t, err.Error(), "truncated")
175 })
176 }
177
178 func TestRunDirectWithOptionsUsageContext(t *testing.T) {
179 if runtime.GOOS == "windows" {
180 t.Skip("uses sh scripts")
181 }
182
183 tmp := t.TempDir()
184 workdir := filepath.Join(tmp, "subdir")
185 require.NoError(t, os.Mkdir(workdir, 0o755))
186
187 writeExe := func(path, body string) {
188 require.NoError(t, os.WriteFile(path, []byte(body), 0o755))
189 }
190
191 script := filepath.Join(tmp, "envpwd.sh")
192 writeExe(script, "#!/bin/sh\nprintf 'PWD=%s\\nFOO=%s\\n' \"$PWD\" \"$FOO\"\n")
193
194 sleeper := filepath.Join(tmp, "sleep.sh")
195 writeExe(sleeper, "#!/bin/sh\nsleep 2\n")
196
197 tests := map[string]struct {
198 timeout time.Duration
199 opts RunOptions
200 binPath string
201 args []string
202 assert func(*testing.T, []byte, string, ResourceUsage, error)
203 }{
204 "honors working directory and explicit environment": {
205 timeout: time.Second,
206 opts: RunOptions{
207 Dir: workdir,
208 Env: []string{"FOO=bar"},
209 },
210 binPath: script,
211 assert: func(t *testing.T, out []byte, cmd string, usage ResourceUsage, err error) {
212 t.Helper()
213 require.NoError(t, err)
214 assert.Contains(t, cmd, script)
215 assert.Contains(t, string(out), "\nFOO=bar\n")
216 assert.Contains(t, string(out), "PWD=")
217 assert.True(t, usage.User >= 0)
218 assert.True(t, usage.System >= 0)
219 },
220 },
221 "timeout is propagated through the direct helper": {
222 timeout: 100 * time.Millisecond,
223 opts: RunOptions{},
224 binPath: sleeper,
225 assert: func(t *testing.T, _ []byte, _ string, _ ResourceUsage, err error) {
226 t.Helper()
227 require.Error(t, err)
228 assert.ErrorIs(t, err, context.DeadlineExceeded)
229 },
230 },
231 }
232
233 for name, tc := range tests {
234 t.Run(name, func(t *testing.T) {
235 out, cmd, usage, err := RunDirectWithOptionsUsageContext(context.Background(), nil, tc.timeout, tc.opts, tc.binPath, tc.args...)
236 tc.assert(t, out, cmd, usage, err)
237 })
238 }
239 }
240
241 func TestFindBinary(t *testing.T) {
242 tmp := t.TempDir()
243
244 binPath := filepath.Join(tmp, "testbin")
245 require.NoError(t, os.WriteFile(binPath, []byte("#!/bin/sh\n"), 0o755))
246
247 t.Run("found in default paths", func(t *testing.T) {
248 path, err := FindBinary(
249 []string{"nonexistent-binary-12345"},
250 []string{filepath.Join(tmp, "missing"), binPath},
251 )
252 require.NoError(t, err)
253 assert.Equal(t, binPath, path)
254 })
255
256 t.Run("not found anywhere", func(t *testing.T) {
257 _, err := FindBinary(
258 []string{"nonexistent-binary-12345"},
259 []string{filepath.Join(tmp, "also-missing")},
260 )
261 require.Error(t, err)
262 assert.Contains(t, err.Error(), "executable not found")
263 })
264
265 t.Run("skips directories", func(t *testing.T) {
266 dirPath := filepath.Join(tmp, "adir")
267 require.NoError(t, os.Mkdir(dirPath, 0o755))
268
269 _, err := FindBinary(
270 []string{"nonexistent-binary-12345"},
271 []string{dirPath},
272 )
273 require.Error(t, err)
274 })
275
276 t.Run("nil names searches only default paths", func(t *testing.T) {
277 path, err := FindBinary(nil, []string{binPath})
278 require.NoError(t, err)
279 assert.Equal(t, binPath, path)
280 })
281
282 t.Run("nil names not found", func(t *testing.T) {
283 _, err := FindBinary(nil, []string{filepath.Join(tmp, "missing")})
284 require.Error(t, err)
285 assert.Contains(t, err.Error(), "executable not found")
286 })
287
288 t.Run("both nil", func(t *testing.T) {
289 _, err := FindBinary(nil, nil)
290 require.Error(t, err)
291 })
292 }
293
294 func TestRunUnprivilegedWithOptionsCmdWorkingDir(t *testing.T) {
295 if runtime.GOOS == "windows" {
296 t.Skip("uses sh scripts")
297 }
298
299 tmp := t.TempDir()
300 workdir := filepath.Join(tmp, "subdir")
301 require.NoError(t, os.Mkdir(workdir, 0o755))
302 script := filepath.Join(tmp, "pwd.sh")
303 require.NoError(t, os.WriteFile(script, []byte("#!/bin/sh\npwd\n"), 0o755))
304
305 helper := filepath.Join(tmp, "helper.sh")
306 require.NoError(t, os.WriteFile(helper, []byte("#!/bin/sh\nexec \"$@\"\n"), 0o755))
307
308 orig := defaultRunner.ndRunPath
309 defaultRunner.ndRunPath = helper
310 defer func() { defaultRunner.ndRunPath = orig }()
311
312 opts := RunOptions{Dir: workdir}
313 out, cmd, err := RunUnprivilegedWithOptionsCmd(nil, time.Second, opts, script)
314 require.NoError(t, err)
315 assert.Contains(t, cmd, script)
316 assert.Equal(t, workdir+"\n", string(out))
317 }
318
319 func TestRunUnprivilegedWithOptionsUsage(t *testing.T) {
320 if runtime.GOOS == "windows" {
321 t.Skip("uses sh scripts")
322 }
323
324 tmp := t.TempDir()
325 script := filepath.Join(tmp, "noop.sh")
326 require.NoError(t, os.WriteFile(script, []byte("#!/bin/sh\nprintf foo"), 0o755))
327
328 helper := filepath.Join(tmp, "helper.sh")
329 require.NoError(t, os.WriteFile(helper, []byte("#!/bin/sh\nexec \"$@\"\n"), 0o755))
330
331 orig := defaultRunner.ndRunPath
332 defaultRunner.ndRunPath = helper
333 defer func() { defaultRunner.ndRunPath = orig }()
334
335 opts := RunOptions{}
336 out, cmd, usage, err := RunUnprivilegedWithOptionsUsage(nil, time.Second, opts, script)
337 require.NoError(t, err)
338 assert.Contains(t, cmd, script)
339 assert.Equal(t, "foo", string(out))
340 assert.True(t, usage.User >= 0)
341 assert.True(t, usage.System >= 0)
342 }