Skip to content

Commit 40674b3

Browse files
committed
reexec: use blackbox testing
Try to use blackbox testing; add a internal/reexecoverride.OverrideArgv0 utility to override `os.Arg[0]` for testing, and to disable the Linux "/proc/self/exe" fast-path to allow us to test the "naive" path resolution. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 627ee9d commit 40674b3

3 files changed

Lines changed: 85 additions & 17 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Package reexecoverride provides test utilities for overriding argv0 as
2+
// observed by reexec.Self within the current process.
3+
4+
package reexecoverride
5+
6+
import (
7+
"sync/atomic"
8+
)
9+
10+
// argv0Override holds an optional override for os.Args[0] used by reexec.Self.
11+
var argv0Override atomic.Pointer[string]
12+
13+
// Argv0 returns the overridden argv0 if set.
14+
func Argv0() (string, bool) {
15+
p := argv0Override.Load()
16+
if p == nil {
17+
return "", false
18+
}
19+
return *p, true
20+
}
21+
22+
// TestingTB is the minimal subset of [testing.TB] used by this package.
23+
type TestingTB interface {
24+
Helper()
25+
Cleanup(func())
26+
}
27+
28+
// OverrideArgv0 overrides the argv0 value observed by reexec.Self for the
29+
// lifetime of the calling test and restores it via [testing.TB.Cleanup].
30+
//
31+
// The override is process-global. Tests using OverrideArgv0 must not run in
32+
// parallel with other tests that call reexec.Self. OverrideArgv0 panics if an
33+
// override is already active.
34+
func OverrideArgv0(t TestingTB, argv0 string) {
35+
t.Helper()
36+
37+
s := argv0
38+
if !argv0Override.CompareAndSwap(nil, &s) {
39+
panic("testing: test using reexecoverride.OverrideArgv0 cannot use t.Parallel")
40+
}
41+
42+
t.Cleanup(func() {
43+
if !argv0Override.CompareAndSwap(&s, nil) {
44+
panic("testing: cleanup for reexecoverride.OverrideArgv0 detected parallel use of reexec.Self")
45+
}
46+
})
47+
}

reexec/reexec.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"os/exec"
1414
"path/filepath"
1515
"runtime"
16+
17+
"github.com/moby/sys/reexec/internal/reexecoverride"
1618
)
1719

1820
var registeredInitializers = make(map[string]func())
@@ -78,6 +80,9 @@ func CommandContext(ctx context.Context, args ...string) *exec.Cmd {
7880
// "my-binary" at "/usr/bin/" (or "my-binary.exe" at "C:\" on Windows),
7981
// then it returns "/usr/bin/my-binary" and "C:\my-binary.exe" respectively.
8082
func Self() string {
83+
if argv0, ok := reexecoverride.Argv0(); ok {
84+
return naiveSelf(argv0)
85+
}
8186
if runtime.GOOS == "linux" {
8287
return "/proc/self/exe"
8388
}

reexec/reexec_test.go

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
package reexec
1+
package reexec_test
22

33
import (
44
"context"
55
"errors"
66
"fmt"
77
"os"
8-
"os/exec"
98
"path/filepath"
109
"reflect"
1110
"runtime"
1211
"strings"
1312
"testing"
1413
"time"
14+
15+
"github.com/moby/sys/reexec"
16+
"github.com/moby/sys/reexec/internal/reexecoverride"
1517
)
1618

1719
const (
@@ -21,23 +23,23 @@ const (
2123
)
2224

2325
func init() {
24-
Register(testReExec, func() {
26+
reexec.Register(testReExec, func() {
2527
panic("Return Error")
2628
})
27-
Register(testReExec2, func() {
29+
reexec.Register(testReExec2, func() {
2830
var args string
2931
if len(os.Args) > 1 {
3032
args = fmt.Sprintf("(args: %#v)", os.Args[1:])
3133
}
3234
fmt.Println("Hello", testReExec2, args)
3335
os.Exit(0)
3436
})
35-
Register(testReExec3, func() {
37+
reexec.Register(testReExec3, func() {
3638
fmt.Println("Hello " + testReExec3)
3739
time.Sleep(1 * time.Second)
3840
os.Exit(0)
3941
})
40-
if Init() {
42+
if reexec.Init() {
4143
// Make sure we exit in case re-exec didn't os.Exit on its own.
4244
os.Exit(0)
4345
}
@@ -73,7 +75,7 @@ func TestRegister(t *testing.T) {
7375
t.Errorf("got %q, want %q", r, tc.expectedErr)
7476
}
7577
}()
76-
Register(tc.name, func() {})
78+
reexec.Register(tc.name, func() {})
7779
})
7880
}
7981
}
@@ -102,7 +104,7 @@ func TestCommand(t *testing.T) {
102104
}
103105
for _, tc := range tests {
104106
t.Run(tc.doc, func(t *testing.T) {
105-
cmd := Command(tc.cmdAndArgs...)
107+
cmd := reexec.Command(tc.cmdAndArgs...)
106108
if !reflect.DeepEqual(cmd.Args, tc.cmdAndArgs) {
107109
t.Fatalf("got %+v, want %+v", cmd.Args, tc.cmdAndArgs)
108110
}
@@ -169,7 +171,7 @@ func TestCommandContext(t *testing.T) {
169171
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
170172
defer cancel()
171173

172-
cmd := CommandContext(ctx, tc.cmdAndArgs...)
174+
cmd := reexec.CommandContext(ctx, tc.cmdAndArgs...)
173175
if !reflect.DeepEqual(cmd.Args, tc.cmdAndArgs) {
174176
t.Fatalf("got %+v, want %+v", cmd.Args, tc.cmdAndArgs)
175177
}
@@ -208,11 +210,10 @@ func TestRunNaiveSelf(t *testing.T) {
208210
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
209211
defer cancel()
210212

211-
// Similar to [rexec.CommandContext], but using naiveSelf to skip the
212-
// optimized "/proc/self/exe" on Linux.
213-
cmd := exec.CommandContext(ctx, naiveSelf(os.Args[0]), testReExec2)
214-
cmd.Args = cmd.Args[1:]
213+
// Force Self() to use naiveSelf(os.Args[0]), instead of "/proc/self/exe" on Linux.
214+
reexecoverride.OverrideArgv0(t, os.Args[0])
215215

216+
cmd := reexec.CommandContext(ctx, testReExec2)
216217
out, err := cmd.CombinedOutput()
217218
if err != nil {
218219
t.Fatalf("Unable to start command: %v", err)
@@ -226,12 +227,24 @@ func TestRunNaiveSelf(t *testing.T) {
226227
}
227228

228229
func TestNaiveSelfResolve(t *testing.T) {
230+
t.Run("fast path on Linux", func(t *testing.T) {
231+
if runtime.GOOS != "linux" {
232+
t.Skip("only supported on Linux")
233+
}
234+
resolved := reexec.Self()
235+
expected := "/proc/self/exe"
236+
if resolved != expected {
237+
t.Errorf("got %v, want %v", resolved, expected)
238+
}
239+
})
229240
t.Run("resolve in PATH", func(t *testing.T) {
230241
executable := "sh"
231242
if runtime.GOOS == "windows" {
232243
executable = "cmd"
233244
}
234-
resolved := naiveSelf(executable)
245+
t.Setenv("PATH", executable)
246+
reexecoverride.OverrideArgv0(t, executable)
247+
resolved := reexec.Self()
235248
if resolved == executable {
236249
t.Errorf("did not resolve via PATH; got %q", resolved)
237250
}
@@ -241,23 +254,26 @@ func TestNaiveSelfResolve(t *testing.T) {
241254
})
242255
t.Run("not in PATH", func(t *testing.T) {
243256
const executable = "some-nonexistent-executable"
244-
resolved := naiveSelf(executable)
257+
reexecoverride.OverrideArgv0(t, executable)
258+
resolved := reexec.Self()
245259
want, _ := filepath.Abs(executable)
246260
if resolved != want {
247261
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
248262
}
249263
})
250264
t.Run("relative path", func(t *testing.T) {
251265
executable := filepath.Join(".", "some-executable")
252-
resolved := naiveSelf(executable)
266+
reexecoverride.OverrideArgv0(t, executable)
267+
resolved := reexec.Self()
253268
want, _ := filepath.Abs(executable)
254269
if resolved != want {
255270
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
256271
}
257272
})
258273
t.Run("absolute path unchanged", func(t *testing.T) {
259274
executable := filepath.Join(os.TempDir(), "some-executable")
260-
resolved := naiveSelf(executable)
275+
reexecoverride.OverrideArgv0(t, executable)
276+
resolved := reexec.Self()
261277
if resolved != executable {
262278
t.Errorf("should not modify absolute paths; got %q, want %q", resolved, executable)
263279
}

0 commit comments

Comments
 (0)