Skip to content

Commit 341240a

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 9e2dfa8 commit 341240a

File tree

3 files changed

+82
-17
lines changed

3 files changed

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

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: 32 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(context.Background(), 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(context.Background(), 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,23 @@ 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+
reexecoverride.OverrideArgv0(t, executable)
246+
resolved := reexec.Self()
235247
if resolved == executable {
236248
t.Errorf("did not resolve via PATH; got %q", resolved)
237249
}
@@ -241,23 +253,26 @@ func TestNaiveSelfResolve(t *testing.T) {
241253
})
242254
t.Run("not in PATH", func(t *testing.T) {
243255
const executable = "some-nonexistent-executable"
244-
resolved := naiveSelf(executable)
256+
reexecoverride.OverrideArgv0(t, executable)
257+
resolved := reexec.Self()
245258
want, _ := filepath.Abs(executable)
246259
if resolved != want {
247260
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
248261
}
249262
})
250263
t.Run("relative path", func(t *testing.T) {
251264
executable := filepath.Join(".", "some-executable")
252-
resolved := naiveSelf(executable)
265+
reexecoverride.OverrideArgv0(t, executable)
266+
resolved := reexec.Self()
253267
want, _ := filepath.Abs(executable)
254268
if resolved != want {
255269
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
256270
}
257271
})
258272
t.Run("absolute path unchanged", func(t *testing.T) {
259273
executable := filepath.Join(os.TempDir(), "some-executable")
260-
resolved := naiveSelf(executable)
274+
reexecoverride.OverrideArgv0(t, executable)
275+
resolved := reexec.Self()
261276
if resolved != executable {
262277
t.Errorf("should not modify absolute paths; got %q, want %q", resolved, executable)
263278
}

0 commit comments

Comments
 (0)