Skip to content

Commit fe369cf

Browse files
committed
reexec: use blackbox testing
Try to use blackbox testing; add a internal/reexectest.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 fe369cf

3 files changed

Lines changed: 84 additions & 17 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Package reexectest provides utilities for user in tests.
2+
3+
package reexectest
4+
5+
import (
6+
"sync"
7+
"sync/atomic"
8+
)
9+
10+
var (
11+
// argv0Override holds an optional override for os.Args[0] used by reexec.Self.
12+
argv0Override atomic.Pointer[string]
13+
14+
// overrideMu serializes setting/clearing the override so tests don't trample
15+
// each other if multiple tests try to override at the same time.
16+
overrideMu sync.Mutex
17+
)
18+
19+
// Argv0 returns the overridden argv0 if set.
20+
func Argv0() (string, bool) {
21+
p := argv0Override.Load()
22+
if p == nil {
23+
return "", false
24+
}
25+
return *p, true
26+
}
27+
28+
// OverrideArgv0 overrides os.Arg[0] with argv0 and returns a restore function.
29+
// Call the restore function (typically via t.Cleanup) to clear the override.
30+
//
31+
// While the override is active, any concurrent call to reexec.Self() in the
32+
// same process will observe the override. So tests that use this should not
33+
// run in parallel with other tests that call reexec.Self().
34+
func OverrideArgv0(argv0 string) (restore func()) {
35+
overrideMu.Lock()
36+
s := argv0
37+
argv0Override.Store(&s)
38+
return func() {
39+
argv0Override.Store(nil)
40+
overrideMu.Unlock()
41+
}
42+
}

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/reexectest"
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 := reexectest.Argv0(); ok {
84+
return naiveSelf(argv0)
85+
}
8186
if runtime.GOOS == "linux" {
8287
return "/proc/self/exe"
8388
}

reexec/reexec_test.go

Lines changed: 37 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/reexectest"
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,11 @@ 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+
restore := reexectest.OverrideArgv0(os.Args[0])
215+
t.Cleanup(restore)
215216

217+
cmd := reexec.CommandContext(ctx, testReExec2)
216218
out, err := cmd.CombinedOutput()
217219
if err != nil {
218220
t.Fatalf("Unable to start command: %v", err)
@@ -226,12 +228,24 @@ func TestRunNaiveSelf(t *testing.T) {
226228
}
227229

228230
func TestNaiveSelfResolve(t *testing.T) {
231+
t.Run("fast path on Linux", func(t *testing.T) {
232+
if runtime.GOOS != "linux" {
233+
t.Skip("only supported on Linux")
234+
}
235+
resolved := reexec.Self()
236+
expected := "/proc/self/exe"
237+
if resolved != expected {
238+
t.Errorf("got %v, want %v", resolved, expected)
239+
}
240+
})
229241
t.Run("resolve in PATH", func(t *testing.T) {
230242
executable := "sh"
231243
if runtime.GOOS == "windows" {
232244
executable = "cmd"
233245
}
234-
resolved := naiveSelf(executable)
246+
restore := reexectest.OverrideArgv0(executable)
247+
t.Cleanup(restore)
248+
resolved := reexec.Self()
235249
if resolved == executable {
236250
t.Errorf("did not resolve via PATH; got %q", resolved)
237251
}
@@ -241,23 +255,29 @@ func TestNaiveSelfResolve(t *testing.T) {
241255
})
242256
t.Run("not in PATH", func(t *testing.T) {
243257
const executable = "some-nonexistent-executable"
244-
resolved := naiveSelf(executable)
258+
restore := reexectest.OverrideArgv0(executable)
259+
t.Cleanup(restore)
260+
resolved := reexec.Self()
245261
want, _ := filepath.Abs(executable)
246262
if resolved != want {
247263
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
248264
}
249265
})
250266
t.Run("relative path", func(t *testing.T) {
251267
executable := filepath.Join(".", "some-executable")
252-
resolved := naiveSelf(executable)
268+
restore := reexectest.OverrideArgv0(executable)
269+
t.Cleanup(restore)
270+
resolved := reexec.Self()
253271
want, _ := filepath.Abs(executable)
254272
if resolved != want {
255273
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
256274
}
257275
})
258276
t.Run("absolute path unchanged", func(t *testing.T) {
259277
executable := filepath.Join(os.TempDir(), "some-executable")
260-
resolved := naiveSelf(executable)
278+
restore := reexectest.OverrideArgv0(executable)
279+
resolved := reexec.Self()
280+
restore()
261281
if resolved != executable {
262282
t.Errorf("should not modify absolute paths; got %q, want %q", resolved, executable)
263283
}

0 commit comments

Comments
 (0)