Repository navigation
Expand file tree
/
Copy pathexec_integration_test.go
More file actions
161 lines (150 loc) · 5.79 KB
/
Copy pathexec_integration_test.go
File metadata and controls
161 lines (150 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//go:build boxlite_dev
package boxlite
import (
"bytes"
"context"
"errors"
"strings"
"testing"
"time"
)
// createStartedBoxOrSkip mirrors createStartedBox but skips (rather than
// fails) when the failure mode is an infrastructure prerequisite — image
// pull (ErrStorage / ErrImage) or network reach (ErrNetwork). Used by
// integration tests that the pre-push hook may run in network-restricted
// environments where docker.io is unreachable.
func createStartedBoxOrSkip(t *testing.T, rt *Runtime, image string, opts ...BoxOption) *Box {
t.Helper()
ctx := context.Background()
box, err := rt.Create(ctx, image, opts...)
if err != nil {
var e *Error
if errors.As(err, &e) && (e.Code == ErrStorage || e.Code == ErrImage || e.Code == ErrNetwork) {
t.Skipf("infrastructure prerequisite unavailable (code=%d): %v", e.Code, err)
}
t.Fatalf("Create: %v", err)
}
t.Cleanup(func() {
_ = box.Stop(ctx)
_ = rt.ForceRemove(ctx, box.ID())
_ = box.Close()
})
if err := box.Start(ctx); err != nil {
var e *Error
if errors.As(err, &e) && (e.Code == ErrStorage || e.Code == ErrImage || e.Code == ErrNetwork) {
t.Skipf("infrastructure prerequisite unavailable on Start (code=%d): %v", e.Code, err)
}
t.Fatalf("Start: %v", err)
}
return box
}
// TestIntegrationExecEnvWorkingDirTimeout proves that the three fields
// added to ExecutionOptions and Cmd in this commit actually reach the
// guest process — i.e. the Go SDK's env_pairs / workdir / timeout_secs
// plumbing makes it through the C FFI and out the other side. A single
// box is reused for all three checks because creating a VM dominates
// the test cost.
//
// Each subtest asserts a project-symbol path:
// - Env: Cmd.Env -> StartExecution -> env_pairs -> printenv
// - WorkingDir: Cmd.Dir -> StartExecution -> workdir -> pwd
// - Timeout: Cmd.Timeout -> StartExecution -> timeout_secs -> SIGKILL
func TestIntegrationExecEnvWorkingDirTimeout(t *testing.T) {
rt := newTestRuntime(t)
box := createStartedBoxOrSkip(t, rt, "alpine:latest", WithAutoRemove(false))
// Short-lived commands (printenv, pwd) can exit so fast that
// execution.Wait() returns before the SDK's async stdout pump has
// delivered the final bytes. Padding the command with a brief sleep
// gives the pump a deterministic drain window without depending on
// host-side timing — better than a wall-clock sleep in the test.
const drainPad = " && sleep 0.1"
t.Run("Env reaches the guest process", func(t *testing.T) {
cmd := box.Command("sh", "-c", "printenv BOXLITE_TEST_KEY"+drainPad)
cmd.Env = map[string]string{
"BOXLITE_TEST_KEY": "bar-from-test",
"BOXLITE_TEST_OTHER": "unused",
}
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(context.Background()); err != nil {
t.Fatalf("Cmd.Run with Env: %v", err)
}
got := strings.TrimSpace(out.String())
if got != "bar-from-test" {
t.Fatalf("env var did not reach guest: want %q, got %q", "bar-from-test", got)
}
})
t.Run("Dir sets the working directory in the guest", func(t *testing.T) {
cmd := box.Command("sh", "-c", "pwd"+drainPad)
cmd.Dir = "/tmp"
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(context.Background()); err != nil {
t.Fatalf("Cmd.Run with Dir: %v", err)
}
got := strings.TrimSpace(out.String())
if got != "/tmp" {
t.Fatalf("working dir did not reach guest: want /tmp, got %q", got)
}
})
t.Run("Timeout kills a long-running process", func(t *testing.T) {
cmd := box.Command("sleep", "30")
cmd.Timeout = 2 * time.Second
start := time.Now()
err := cmd.Run(context.Background())
elapsed := time.Since(start)
// The exec should NOT have run to completion. We accept either
// (a) Run returning a non-nil error, or (b) a non-zero exit
// code observed via Cmd.ExitCode — either way the guest must
// have been killed well before 30s elapsed.
if elapsed >= 15*time.Second {
t.Fatalf("Timeout did not curtail the exec: elapsed=%s, err=%v", elapsed, err)
}
if err == nil && cmd.ExitCode() == 0 {
t.Fatalf("expected non-zero exit or error after Timeout; got success in %s", elapsed)
}
})
}
// TestIntegrationExecStdoutRace asserts that Execution.Wait does not
// return until every stdout chunk has been delivered to the user's sink
// — i.e. the drain barrier introduced for #563 holds. Pre-fix, the C
// Wait task was decoupled from the stream pumps, so a short command
// could push its Wait queue event before the late Stdout chunks landed,
// and Cmd.Run would return with a truncated buffer. The pre-existing
// `TestIntegrationExecEnvWorkingDirTimeout` papers over this with a
// `&& sleep 0.1` pad on every command (see drainPad). This test
// deliberately does NOT pad the command — that's the whole point.
//
// Repeated rounds because the race is timing-sensitive: a single
// fast/lucky run can mask it. ROUNDS=20 has caught the regression
// reliably on this machine.
func TestIntegrationExecStdoutRace(t *testing.T) {
rt := newTestRuntime(t)
box := createStartedBoxOrSkip(t, rt, "alpine:latest", WithAutoRemove(false))
const rounds = 20
const want = "marker-line\n"
var truncated int
for i := 0; i < rounds; i++ {
cmd := box.Command("sh", "-c", "echo marker-line")
var buf bytes.Buffer
cmd.Stdout = &buf
if err := cmd.Run(context.Background()); err != nil {
t.Fatalf("round %d: Cmd.Run: %v", i, err)
}
if buf.String() != want {
truncated++
t.Logf("round %d: stdout=%q (want %q)", i, buf.String(), want)
}
}
if truncated > 0 {
t.Fatalf(
"Execution.Wait returned before %d/%d stdout deliveries — "+
"the #563 race regressed. Either Execution.Wait's "+
"post-reap drain step (<-streamState.drained, the "+
"os/exec awaitGoroutines analog) was removed, or the "+
"C-side exit_pump no longer awaits the stream_done_rx "+
"oneshots that close that channel.",
truncated, rounds,
)
}
}