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}