v0.1.0 cbb8ad6

frame_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package execstream
4
5import (
6	"bytes"
7	"encoding/binary"
8	"errors"
9	"io"
10	"net"
11	"strings"
12	"testing"
13	"testing/iotest"
14	"time"
15
16	"bnema.dev/ermite-runner/internal/execstream/execpb"
17)
18
19func TestRoundTrip(t *testing.T) {
20	var buf bytes.Buffer
21	c := NewConn(&buf)
22	in := []*execpb.Frame{
23		{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Version: "v", Sandbox: "vm"}}},
24		{Body: &execpb.Frame_Output{Output: &execpb.Output{Stream: execpb.StreamKind_STREAM_KIND_STDERR, Data: []byte("x\x00y")}}},
25		{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 3}}},
26	}
27	for _, f := range in {
28		if err := c.Send(f); err != nil {
29			t.Fatal(err)
30		}
31	}
32	// One byte at a time: framing must not depend on read sizes.
33	r := NewConn(struct {
34		io.Reader
35		io.Writer
36	}{iotest.OneByteReader(&buf), io.Discard})
37	for i := range in {
38		out, err := r.Recv()
39		if err != nil {
40			t.Fatalf("frame %d: %v", i, err)
41		}
42		if out.String() != in[i].String() {
43			t.Fatalf("frame %d: got %v want %v", i, out, in[i])
44		}
45	}
46	if _, err := r.Recv(); err != io.EOF {
47		t.Fatalf("want io.EOF at end, got %v", err)
48	}
49}
50
51func TestRecvErrors(t *testing.T) {
52	huge := make([]byte, 4)
53	binary.BigEndian.PutUint32(huge, MaxFrame+1)
54	if _, err := NewConn(bytes.NewBuffer(huge)).Recv(); !errors.Is(err, ErrFrameTooLarge) {
55		t.Fatalf("oversize: %v", err)
56	}
57	trunc := []byte{0, 0, 0, 9, 1, 2}
58	if _, err := NewConn(bytes.NewBuffer(trunc)).Recv(); !errors.Is(err, io.ErrUnexpectedEOF) {
59		t.Fatalf("truncated body: %v", err)
60	}
61	if _, err := NewConn(bytes.NewBuffer([]byte{0, 0})).Recv(); !errors.Is(err, io.ErrUnexpectedEOF) {
62		t.Fatalf("truncated header: %v", err)
63	}
64	garbage := []byte{0, 0, 0, 3, 0xff, 0xff, 0xff}
65	if _, err := NewConn(bytes.NewBuffer(garbage)).Recv(); err == nil {
66		t.Fatal("garbage payload accepted")
67	}
68}
69
70func TestSendTooLarge(t *testing.T) {
71	c := NewConn(struct {
72		io.Reader
73		io.Writer
74	}{bytes.NewReader(nil), io.Discard})
75	err := c.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{Data: make([]byte, MaxFrame)}}})
76	if !errors.Is(err, ErrFrameTooLarge) {
77		t.Fatalf("got %v", err)
78	}
79}
80
81func TestDrive(t *testing.T) {
82	host, guest := net.Pipe()
83	defer func() { _ = host.Close() }()
84	go func() {
85		defer func() { _ = guest.Close() }()
86		g := NewConn(guest)
87		_ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Sandbox: "container"}}})
88		f, err := g.Recv()
89		if err != nil || f.GetRun() == nil || f.GetRun().Script != "echo hi" {
90			return
91		}
92		_ = g.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{Data: []byte("hi\n")}}})
93		_ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 7}}})
94	}()
95	var got []byte
96	hello, exit, err := Drive(host, &execpb.Run{Script: "echo hi"}, nil, func(o *execpb.Output) { got = append(got, o.Data...) })
97	if err != nil {
98		t.Fatal(err)
99	}
100	if hello.Sandbox != "container" || exit.Code != 7 || string(got) != "hi\n" {
101		t.Fatalf("hello=%v exit=%v out=%q", hello, exit, got)
102	}
103}
104
105func TestDriveCancelSendsCancel(t *testing.T) {
106	host, guest := net.Pipe()
107	defer func() { _ = host.Close() }()
108	gotCancel := make(chan bool, 1)
109	go func() {
110		defer func() { _ = guest.Close() }()
111		g := NewConn(guest)
112		_ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})
113		_, _ = g.Recv() // run
114		f, err := g.Recv()
115		gotCancel <- err == nil && f.GetCancel() != nil && f.GetCancel().Reason == "cancelled by the user"
116		_ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 130}}})
117	}()
118	cancel := make(chan *execpb.Cancel, 1)
119	time.AfterFunc(50*time.Millisecond, func() { cancel <- &execpb.Cancel{Reason: "cancelled by the user"} })
120	_, exit, err := Drive(host, &execpb.Run{}, cancel, nil)
121	if err != nil || exit.Code != 130 {
122		t.Fatalf("exit=%v err=%v", exit, err)
123	}
124	if !<-gotCancel {
125		t.Fatal("guest did not receive a Cancel frame")
126	}
127}
128
129func TestDriveHelloMissing(t *testing.T) {
130	host, guest := net.Pipe()
131	defer func() { _ = host.Close() }()
132	go func() {
133		defer func() { _ = guest.Close() }()
134		_ = NewConn(guest).Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})
135	}()
136	if _, _, err := Drive(host, &execpb.Run{}, nil, nil); err == nil {
137		t.Fatal("want error when the first frame is not hello")
138	}
139}
140
141func TestDriveJobDeliversJobFrames(t *testing.T) {
142	host, guest := net.Pipe()
143	go func() {
144		g := NewConn(guest)
145		_ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Version: "v"}}})
146		r, _ := g.Recv()
147		if r.GetRun().GetJob().GetJobKey() != "build" {
148			t.Errorf("run: %v", r)
149		}
150		for _, b := range []*execpb.Frame{
151			{Body: &execpb.Frame_StepStarted{StepStarted: &execpb.StepStarted{Number: 1, Name: "Set up job"}}},
152			{Body: &execpb.Frame_Log{Log: &execpb.Log{Step: 1, Text: "hello", UnixNano: 5}}},
153			{Body: &execpb.Frame_StepCompleted{StepCompleted: &execpb.StepCompleted{Number: 1, Conclusion: execpb.StepConclusion_STEP_CONCLUSION_SUCCESS}}},
154			{Body: &execpb.Frame_JobResult{JobResult: &execpb.JobResult{Conclusion: execpb.StepConclusion_STEP_CONCLUSION_SUCCESS, Outputs: map[string]string{"a": "b"}}}},
155			{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}},
156		} {
157			_ = g.Send(b)
158		}
159	}()
160	var got []string
161	_, exit, err := DriveJob(host, &execpb.Run{Job: &execpb.JobSpec{JobKey: "build"}}, nil, JobHandler{
162		StepStarted:   func(s *execpb.StepStarted) { got = append(got, "start:"+s.Name) },
163		Log:           func(l *execpb.Log) { got = append(got, "log:"+l.Text) },
164		StepCompleted: func(s *execpb.StepCompleted) { got = append(got, "done:"+s.Conclusion.String()) },
165		JobResult:     func(r *execpb.JobResult) { got = append(got, "result:"+r.Outputs["a"]) },
166	})
167	if err != nil || exit == nil {
168		t.Fatal(err)
169	}
170	want := "start:Set up job log:hello done:STEP_CONCLUSION_SUCCESS result:b"
171	if strings.Join(got, " ") != want {
172		t.Fatalf("%v", got)
173	}
174}

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