Skip to content

Commit e3e0efe

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 5b8f99b commit e3e0efe

3 files changed

Lines changed: 85 additions & 20 deletions

File tree

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: 35 additions & 20 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
}
@@ -202,18 +204,17 @@ func TestCommandContext(t *testing.T) {
202204
// can resolve a path that can be used to re-execute the current test binary
203205
// when it falls back to the argv[0]-based implementation.
204206
//
205-
// It invokes the binary via naiveSelf (intentionally bypassing the Linux
206-
// /proc/self/exe fast-path) so the fallback logic is exercised consistently
207-
// across platforms.
207+
// It forces Self() to bypass the Linux /proc/self/exe fast-path via
208+
// [reexecoverride.OverrideArgv0] so that the fallback logic is exercised
209+
// consistently across platforms.
208210
func TestRunNaiveSelf(t *testing.T) {
209211
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
210212
defer cancel()
211213

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

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

229230
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+
})
230241
t.Run("resolve in PATH", func(t *testing.T) {
231242
executable := "sh"
232243
if runtime.GOOS == "windows" {
233244
executable = "cmd"
234245
}
235-
resolved := naiveSelf(executable)
246+
reexecoverride.OverrideArgv0(t, executable)
247+
resolved := reexec.Self()
236248
if resolved == executable {
237249
t.Errorf("did not resolve via PATH; got %q", resolved)
238250
}
@@ -242,23 +254,26 @@ func TestNaiveSelfResolve(t *testing.T) {
242254
})
243255
t.Run("not in PATH", func(t *testing.T) {
244256
const executable = "some-nonexistent-executable"
245-
resolved := naiveSelf(executable)
257+
reexecoverride.OverrideArgv0(t, executable)
258+
resolved := reexec.Self()
246259
want, _ := filepath.Abs(executable)
247260
if resolved != want {
248261
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
249262
}
250263
})
251264
t.Run("relative path", func(t *testing.T) {
252265
executable := filepath.Join(".", "some-executable")
253-
resolved := naiveSelf(executable)
266+
reexecoverride.OverrideArgv0(t, executable)
267+
resolved := reexec.Self()
254268
want, _ := filepath.Abs(executable)
255269
if resolved != want {
256270
t.Errorf("expected absolute path; got %q, want %q", resolved, want)
257271
}
258272
})
259273
t.Run("absolute path unchanged", func(t *testing.T) {
260274
executable := filepath.Join(os.TempDir(), "some-executable")
261-
resolved := naiveSelf(executable)
275+
reexecoverride.OverrideArgv0(t, executable)
276+
resolved := reexec.Self()
262277
if resolved != executable {
263278
t.Errorf("should not modify absolute paths; got %q, want %q", resolved, executable)
264279
}

0 commit comments

Comments
 (0)