detached 8ec9ad2

test: wait for the ready line or the first frame instead of sleeping (G8)

commit
8ec9ad20c0fa9a7089c6bec237eaa9c64028f097
author
brice · · 2026-10-12 19:40 UTC
parent
3530513
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +28 -11
M internal/executor/job_test.go +2 -1
@@ -278,9 +278,10 @@ func TestCancelSendsSIGINTThenStopsTheJob(t *testing.T) {
278278 needTools(t, "bash")
279279 c := NewCanceller()
280280 started := make(chan struct{})
281+ // The script prints "ready" after it installed its trap: the cancel can come
282+ // at once.
281283 go func() {
282284 <-started
283- time.Sleep(300 * time.Millisecond)
284285 c.Cancel(CancelUser)
285286 }()
286287 rec := newRecorder()
@@ -278,9 +278,10 @@ func TestCancelSendsSIGINTThenStopsTheJob(t *testing.T) {
278 needTools(t, "bash")278 needTools(t, "bash")
279 c := NewCanceller()279 c := NewCanceller()
280 started := make(chan struct{})280 started := make(chan struct{})
281+ // The script prints "ready" after it installed its trap: the cancel can come
282+ // at once.
281 go func() {283 go func() {
282 <-started284 <-started
283- time.Sleep(300 * time.Millisecond)
284 c.Cancel(CancelUser)285 c.Cancel(CancelUser)
285 }()286 }()
286 rec := newRecorder()287 rec := newRecorder()
M internal/guest/jobexec_test.go +26 -10
@@ -18,7 +18,7 @@ import (
1818 )
1919
2020 // runJobSession starts Exec on a unix socket and drives it with a job.
21-func runJobSession(t *testing.T, yaml string, cancelAfter time.Duration, tweak func(*ExecConfig)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {
21+func runJobSession(t *testing.T, yaml string, cancelOn string, tweak func(*ExecConfig)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {
2222 t.Helper()
2323 sock := filepath.Join(t.TempDir(), "s")
2424 ln, err := net.Listen("unix", sock)
@@ -45,11 +45,13 @@ func runJobSession(t *testing.T, yaml string, cancelAfter time.Duration, tweak f
4545 t.Fatal(err)
4646 }
4747 defer func() { _ = conn.Close() }()
48+ // With cancelOn, the job is cancelled when it logs that line (the script
49+ // prints it once it is ready to be interrupted): no timer to guess.
4850 var cancel chan *execpb.Cancel
49- if cancelAfter > 0 {
51+ if cancelOn != "" {
5052 cancel = make(chan *execpb.Cancel, 1)
51- time.AfterFunc(cancelAfter, func() { cancel <- &execpb.Cancel{Reason: "test", Kind: execpb.CancelKind_CANCEL_KIND_USER} })
5253 }
54+ var cancelOnce sync.Once
5355 var mu sync.Mutex
5456 _, exit, err = execstream.DriveJob(conn, &execpb.Run{Job: &execpb.JobSpec{
5557 WorkflowPath: ".github/workflows/ci.yml", WorkflowYaml: yaml, JobKey: "j", Repo: "hello", RunnerName: "r", RunnerVersion: "v",
@@ -60,7 +62,12 @@ func runJobSession(t *testing.T, yaml string, cancelAfter time.Duration, tweak f
6062 steps = append(steps, s.Name)
6163 mu.Unlock()
6264 },
63- Log: func(l *execpb.Log) { logs = append(logs, l) },
65+ Log: func(l *execpb.Log) {
66+ logs = append(logs, l)
67+ if cancelOn != "" && l.Text == cancelOn {
68+ cancelOnce.Do(func() { cancel <- &execpb.Cancel{Reason: "test", Kind: execpb.CancelKind_CANCEL_KIND_USER} })
69+ }
70+ },
6471 JobResult: func(r *execpb.JobResult) { res = r },
6572 })
6673 if err != nil {
@@ -79,7 +86,7 @@ func requireBash(t *testing.T) {
7986
8087 func TestExecRunsAJob(t *testing.T) {
8188 requireBash(t)
82- steps, logs, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n outputs:\n o: ${{ steps.s.outputs.v }}\n steps:\n - id: s\n run: |\n echo hello-from-job\n echo v=42 >> \"$GITHUB_OUTPUT\"\n", 0, nil)
89+ steps, logs, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n outputs:\n o: ${{ steps.s.outputs.v }}\n steps:\n - id: s\n run: |\n echo hello-from-job\n echo v=42 >> \"$GITHUB_OUTPUT\"\n", "", nil)
8390 if res == nil || res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS || res.Outputs["o"] != "42" || exit.Code != 0 {
8491 t.Fatalf("res=%v exit=%v", res, exit)
8592 }
@@ -99,7 +106,7 @@ func TestExecRunsAJob(t *testing.T) {
99106
100107 func TestExecJobFailureExitsNonZero(t *testing.T) {
101108 requireBash(t)
102- _, _, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: exit 4\n", 0, nil)
109+ _, _, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: exit 4\n", "", nil)
103110 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_FAILURE || exit.Code == 0 {
104111 t.Fatalf("res=%v exit=%v", res, exit)
105112 }
@@ -107,7 +114,7 @@ func TestExecJobFailureExitsNonZero(t *testing.T) {
107114
108115 func TestExecJobLongLinesAreContinued(t *testing.T) {
109116 requireBash(t)
110- _, logs, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: head -c 150000 /dev/zero | tr '\\0' y; echo\n", 0, nil)
117+ _, logs, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: head -c 150000 /dev/zero | tr '\\0' y; echo\n", "", nil)
111118 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS {
112119 t.Fatalf("%v", res)
113120 }
@@ -130,7 +137,7 @@ func TestExecJobLongLinesAreContinued(t *testing.T) {
130137 func TestExecJobCancel(t *testing.T) {
131138 requireBash(t)
132139 start := time.Now()
133- _, _, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: |\n echo ready\n sleep 30\n", 700*time.Millisecond, nil)
140+ _, _, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: |\n echo ready\n sleep 30\n", "ready", nil)
134141 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_CANCELLED || res.RunnerShutdown {
135142 t.Fatalf("%v", res)
136143 }
@@ -161,10 +168,19 @@ func TestExecJobHostGoneIsAShutdown(t *testing.T) {
161168 t.Fatal(err)
162169 }
163170 _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: &execpb.Run{Job: &execpb.JobSpec{
164- WorkflowYaml: "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: sleep 60\n", JobKey: "j", Repo: "hello",
171+ WorkflowYaml: "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: echo running; sleep 60\n", JobKey: "j", Repo: "hello",
165172 Contexts: `{"github":{"repository":"o/hello"}}`,
166173 }}}})
167- time.Sleep(700 * time.Millisecond)
174+ // Close the stream once the step is running (its first line arrived).
175+ for {
176+ f, err := c.Recv()
177+ if err != nil {
178+ t.Fatal(err)
179+ }
180+ if l := f.GetLog(); l != nil && l.Text == "running" {
181+ break
182+ }
183+ }
168184 _ = conn.Close()
169185 select {
170186 case <-done:
@@ -18,7 +18,7 @@ import (
18 )18 )
19 19
20 // runJobSession starts Exec on a unix socket and drives it with a job.20 // runJobSession starts Exec on a unix socket and drives it with a job.
21-func runJobSession(t *testing.T, yaml string, cancelAfter time.Duration, tweak func(*ExecConfig)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {21+func runJobSession(t *testing.T, yaml string, cancelOn string, tweak func(*ExecConfig)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {
22 t.Helper()22 t.Helper()
23 sock := filepath.Join(t.TempDir(), "s")23 sock := filepath.Join(t.TempDir(), "s")
24 ln, err := net.Listen("unix", sock)24 ln, err := net.Listen("unix", sock)
@@ -45,11 +45,13 @@ func runJobSession(t *testing.T, yaml string, cancelAfter time.Duration, tweak f
45 t.Fatal(err)45 t.Fatal(err)
46 }46 }
47 defer func() { _ = conn.Close() }()47 defer func() { _ = conn.Close() }()
48+ // With cancelOn, the job is cancelled when it logs that line (the script
49+ // prints it once it is ready to be interrupted): no timer to guess.
48 var cancel chan *execpb.Cancel50 var cancel chan *execpb.Cancel
49- if cancelAfter > 0 {51+ if cancelOn != "" {
50 cancel = make(chan *execpb.Cancel, 1)52 cancel = make(chan *execpb.Cancel, 1)
51- time.AfterFunc(cancelAfter, func() { cancel <- &execpb.Cancel{Reason: "test", Kind: execpb.CancelKind_CANCEL_KIND_USER} })
52 }53 }
54+ var cancelOnce sync.Once
53 var mu sync.Mutex55 var mu sync.Mutex
54 _, exit, err = execstream.DriveJob(conn, &execpb.Run{Job: &execpb.JobSpec{56 _, exit, err = execstream.DriveJob(conn, &execpb.Run{Job: &execpb.JobSpec{
55 WorkflowPath: ".github/workflows/ci.yml", WorkflowYaml: yaml, JobKey: "j", Repo: "hello", RunnerName: "r", RunnerVersion: "v",57 WorkflowPath: ".github/workflows/ci.yml", WorkflowYaml: yaml, JobKey: "j", Repo: "hello", RunnerName: "r", RunnerVersion: "v",
@@ -60,7 +62,12 @@ func runJobSession(t *testing.T, yaml string, cancelAfter time.Duration, tweak f
60 steps = append(steps, s.Name)62 steps = append(steps, s.Name)
61 mu.Unlock()63 mu.Unlock()
62 },64 },
63- Log: func(l *execpb.Log) { logs = append(logs, l) },65+ Log: func(l *execpb.Log) {
66+ logs = append(logs, l)
67+ if cancelOn != "" && l.Text == cancelOn {
68+ cancelOnce.Do(func() { cancel <- &execpb.Cancel{Reason: "test", Kind: execpb.CancelKind_CANCEL_KIND_USER} })
69+ }
70+ },
64 JobResult: func(r *execpb.JobResult) { res = r },71 JobResult: func(r *execpb.JobResult) { res = r },
65 })72 })
66 if err != nil {73 if err != nil {
@@ -79,7 +86,7 @@ func requireBash(t *testing.T) {
79 86
80 func TestExecRunsAJob(t *testing.T) {87 func TestExecRunsAJob(t *testing.T) {
81 requireBash(t)88 requireBash(t)
82- steps, logs, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n outputs:\n o: ${{ steps.s.outputs.v }}\n steps:\n - id: s\n run: |\n echo hello-from-job\n echo v=42 >> \"$GITHUB_OUTPUT\"\n", 0, nil)89+ steps, logs, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n outputs:\n o: ${{ steps.s.outputs.v }}\n steps:\n - id: s\n run: |\n echo hello-from-job\n echo v=42 >> \"$GITHUB_OUTPUT\"\n", "", nil)
83 if res == nil || res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS || res.Outputs["o"] != "42" || exit.Code != 0 {90 if res == nil || res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS || res.Outputs["o"] != "42" || exit.Code != 0 {
84 t.Fatalf("res=%v exit=%v", res, exit)91 t.Fatalf("res=%v exit=%v", res, exit)
85 }92 }
@@ -99,7 +106,7 @@ func TestExecRunsAJob(t *testing.T) {
99 106
100 func TestExecJobFailureExitsNonZero(t *testing.T) {107 func TestExecJobFailureExitsNonZero(t *testing.T) {
101 requireBash(t)108 requireBash(t)
102- _, _, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: exit 4\n", 0, nil)109+ _, _, res, exit := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: exit 4\n", "", nil)
103 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_FAILURE || exit.Code == 0 {110 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_FAILURE || exit.Code == 0 {
104 t.Fatalf("res=%v exit=%v", res, exit)111 t.Fatalf("res=%v exit=%v", res, exit)
105 }112 }
@@ -107,7 +114,7 @@ func TestExecJobFailureExitsNonZero(t *testing.T) {
107 114
108 func TestExecJobLongLinesAreContinued(t *testing.T) {115 func TestExecJobLongLinesAreContinued(t *testing.T) {
109 requireBash(t)116 requireBash(t)
110- _, logs, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: head -c 150000 /dev/zero | tr '\\0' y; echo\n", 0, nil)117+ _, logs, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: head -c 150000 /dev/zero | tr '\\0' y; echo\n", "", nil)
111 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS {118 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS {
112 t.Fatalf("%v", res)119 t.Fatalf("%v", res)
113 }120 }
@@ -130,7 +137,7 @@ func TestExecJobLongLinesAreContinued(t *testing.T) {
130 func TestExecJobCancel(t *testing.T) {137 func TestExecJobCancel(t *testing.T) {
131 requireBash(t)138 requireBash(t)
132 start := time.Now()139 start := time.Now()
133- _, _, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: |\n echo ready\n sleep 30\n", 700*time.Millisecond, nil)140+ _, _, res, _ := runJobSession(t, "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: |\n echo ready\n sleep 30\n", "ready", nil)
134 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_CANCELLED || res.RunnerShutdown {141 if res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_CANCELLED || res.RunnerShutdown {
135 t.Fatalf("%v", res)142 t.Fatalf("%v", res)
136 }143 }
@@ -161,10 +168,19 @@ func TestExecJobHostGoneIsAShutdown(t *testing.T) {
161 t.Fatal(err)168 t.Fatal(err)
162 }169 }
163 _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: &execpb.Run{Job: &execpb.JobSpec{170 _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: &execpb.Run{Job: &execpb.JobSpec{
164- WorkflowYaml: "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: sleep 60\n", JobKey: "j", Repo: "hello",171+ WorkflowYaml: "on: push\njobs:\n j:\n runs-on: x\n steps:\n - run: echo running; sleep 60\n", JobKey: "j", Repo: "hello",
165 Contexts: `{"github":{"repository":"o/hello"}}`,172 Contexts: `{"github":{"repository":"o/hello"}}`,
166 }}}})173 }}}})
167- time.Sleep(700 * time.Millisecond)174+ // Close the stream once the step is running (its first line arrived).
175+ for {
176+ f, err := c.Recv()
177+ if err != nil {
178+ t.Fatal(err)
179+ }
180+ if l := f.GetLog(); l != nil && l.Text == "running" {
181+ break
182+ }
183+ }
168 _ = conn.Close()184 _ = conn.Close()
169 select {185 select {
170 case <-done:186 case <-done:

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close