github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/util/ioset/mux_test.go (about) 1 package ioset 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "regexp" 8 "strings" 9 "testing" 10 11 "golang.org/x/sync/errgroup" 12 ) 13 14 // TestMuxIO tests MuxIO 15 func TestMuxIO(t *testing.T) { 16 tests := []struct { 17 name string 18 inputs []instruction 19 initIdx int 20 outputsNum int 21 wants []string 22 23 // Everytime string is written to the mux stdin, the output end 24 // that received the string write backs to the string that is masked with 25 // its index number. This is useful to check if writeback is written from the 26 // expected output destination. 27 wantsMaskedOutput string 28 }{ 29 { 30 name: "single output", 31 inputs: []instruction{ 32 input("foo\nbar\n"), 33 toggle(), 34 input("1234"), 35 toggle(), 36 input("456"), 37 }, 38 initIdx: 0, 39 outputsNum: 1, 40 wants: []string{"foo\nbar\n1234456"}, 41 wantsMaskedOutput: `^0+$`, 42 }, 43 { 44 name: "multi output", 45 inputs: []instruction{ 46 input("foo\nbar\n"), 47 toggle(), 48 input("12" + string([]rune{rune(1)}) + "34abc"), 49 toggle(), 50 input("456"), 51 }, 52 initIdx: 0, 53 outputsNum: 3, 54 wants: []string{"foo\nbar\n", "1234abc", "456"}, 55 wantsMaskedOutput: `^0+1+2+$`, 56 }, 57 { 58 name: "multi output with nonzero index", 59 inputs: []instruction{ 60 input("foo\nbar\n"), 61 toggle(), 62 input("1234"), 63 toggle(), 64 input("456"), 65 }, 66 initIdx: 1, 67 outputsNum: 3, 68 wants: []string{"456", "foo\nbar\n", "1234"}, 69 wantsMaskedOutput: `^1+2+0+$`, 70 }, 71 { 72 name: "multi output many toggles", 73 inputs: []instruction{ 74 input("foo\nbar\n"), 75 toggle(), 76 input("1234"), 77 toggle(), 78 toggle(), 79 input("456"), 80 toggle(), 81 input("%%%%"), 82 toggle(), 83 toggle(), 84 toggle(), 85 input("aaaa"), 86 }, 87 initIdx: 0, 88 outputsNum: 3, 89 wants: []string{"foo\nbar\n456", "1234%%%%aaaa", ""}, 90 wantsMaskedOutput: `^0+1+0+1+$`, 91 }, 92 { 93 name: "enable disable", 94 inputs: []instruction{ 95 input("foo\nbar\n"), 96 toggle(), 97 input("1234"), 98 toggle(), 99 input("456"), 100 disable(2), 101 input("%%%%"), 102 enable(2), 103 toggle(), 104 toggle(), 105 input("aaa"), 106 disable(2), 107 disable(1), 108 input("1111"), 109 toggle(), 110 input("2222"), 111 toggle(), 112 input("3333"), 113 }, 114 initIdx: 0, 115 outputsNum: 3, 116 wants: []string{"foo\nbar\n%%%%111122223333", "1234", "456aaa"}, 117 wantsMaskedOutput: `^0+1+2+0+2+0+$`, 118 }, 119 } 120 121 for _, tt := range tests { 122 t.Run(tt.name, func(t *testing.T) { 123 inBuf, end, in := newTestIn() 124 var outBufs []*outBuf 125 var outs []MuxOut 126 if tt.outputsNum != len(tt.wants) { 127 t.Fatalf("wants != outputsNum") 128 } 129 for i := 0; i < tt.outputsNum; i++ { 130 outBuf, out := newTestOut(i) 131 outBufs = append(outBufs, outBuf) 132 outs = append(outs, MuxOut{out, nil, nil}) 133 } 134 mio := NewMuxIO(in, outs, tt.initIdx, func(prev int, res int) string { return "" }) 135 for _, i := range tt.inputs { 136 // Add input to MuxIO 137 istr, writeback := i(mio) 138 if _, err := end.Stdin.Write([]byte(istr)); err != nil { 139 t.Fatalf("failed to write data to stdin: %v", err) 140 } 141 142 // Wait for writeback of this input 143 var eg errgroup.Group 144 eg.Go(func() error { 145 outbuf := make([]byte, len(writeback)) 146 if _, err := io.ReadAtLeast(end.Stdout, outbuf, len(outbuf)); err != nil { 147 return err 148 } 149 return nil 150 }) 151 eg.Go(func() error { 152 errbuf := make([]byte, len(writeback)) 153 if _, err := io.ReadAtLeast(end.Stderr, errbuf, len(errbuf)); err != nil { 154 return err 155 } 156 return nil 157 }) 158 if err := eg.Wait(); err != nil { 159 t.Fatalf("failed to wait for output: %v", err) 160 } 161 } 162 163 // Close stdin on this MuxIO 164 end.Stdin.Close() 165 166 // Wait for all output ends reach EOF 167 mio.waitClosed() 168 169 // Close stdout/stderr as well 170 in.Close() 171 172 // Check if each output end received expected string 173 <-inBuf.doneCh 174 for i, o := range outBufs { 175 <-o.doneCh 176 if o.stdin != tt.wants[i] { 177 t.Fatalf("output[%d]: got %q; wanted %q", i, o.stdin, tt.wants[i]) 178 } 179 } 180 181 // Check if expected string is returned from expected outputs 182 if !regexp.MustCompile(tt.wantsMaskedOutput).MatchString(inBuf.stdout) { 183 t.Fatalf("stdout: got %q; wanted %q", inBuf.stdout, tt.wantsMaskedOutput) 184 } 185 if !regexp.MustCompile(tt.wantsMaskedOutput).MatchString(inBuf.stderr) { 186 t.Fatalf("stderr: got %q; wanted %q", inBuf.stderr, tt.wantsMaskedOutput) 187 } 188 }) 189 } 190 } 191 192 type instruction func(m *MuxIO) (intput string, writeBackView string) 193 194 func input(s string) instruction { 195 return func(m *MuxIO) (string, string) { 196 return s, strings.ReplaceAll(s, string([]rune{rune(1)}), "") 197 } 198 } 199 200 func toggle() instruction { 201 return func(m *MuxIO) (string, string) { 202 return string([]rune{rune(1)}) + "c", "" 203 } 204 } 205 206 func enable(i int) instruction { 207 return func(m *MuxIO) (string, string) { 208 m.Enable(i) 209 return "", "" 210 } 211 } 212 213 func disable(i int) instruction { 214 return func(m *MuxIO) (string, string) { 215 m.Disable(i) 216 return "", "" 217 } 218 } 219 220 type inBuf struct { 221 stdout string 222 stderr string 223 doneCh chan struct{} 224 } 225 226 func newTestIn() (*inBuf, Out, In) { 227 ti := &inBuf{ 228 doneCh: make(chan struct{}), 229 } 230 gotOutR, gotOutW := io.Pipe() 231 gotErrR, gotErrW := io.Pipe() 232 outR, outW := io.Pipe() 233 var eg errgroup.Group 234 eg.Go(func() error { 235 buf := new(bytes.Buffer) 236 if _, err := io.Copy(io.MultiWriter(gotOutW, buf), outR); err != nil { 237 return err 238 } 239 ti.stdout = buf.String() 240 return nil 241 }) 242 errR, errW := io.Pipe() 243 eg.Go(func() error { 244 buf := new(bytes.Buffer) 245 if _, err := io.Copy(io.MultiWriter(gotErrW, buf), errR); err != nil { 246 return err 247 } 248 ti.stderr = buf.String() 249 return nil 250 }) 251 go func() { 252 eg.Wait() 253 close(ti.doneCh) 254 }() 255 inR, inW := io.Pipe() 256 return ti, Out{Stdin: inW, Stdout: gotOutR, Stderr: gotErrR}, In{Stdin: inR, Stdout: outW, Stderr: errW} 257 } 258 259 type outBuf struct { 260 idx int 261 stdin string 262 doneCh chan struct{} 263 } 264 265 func newTestOut(idx int) (*outBuf, Out) { 266 to := &outBuf{ 267 idx: idx, 268 doneCh: make(chan struct{}), 269 } 270 inR, inW := io.Pipe() 271 outR, outW := io.Pipe() 272 errR, errW := io.Pipe() 273 go func() { 274 defer inR.Close() 275 defer outW.Close() 276 defer errW.Close() 277 buf := new(bytes.Buffer) 278 mw := io.MultiWriter(buf, 279 writeMasked(outW, fmt.Sprintf("%d", to.idx)), 280 writeMasked(errW, fmt.Sprintf("%d", to.idx)), 281 ) 282 if _, err := io.Copy(mw, inR); err != nil { 283 inR.CloseWithError(err) 284 outW.CloseWithError(err) 285 errW.CloseWithError(err) 286 return 287 } 288 to.stdin = buf.String() 289 outW.Close() 290 errW.Close() 291 close(to.doneCh) 292 }() 293 return to, Out{Stdin: inW, Stdout: outR, Stderr: errR} 294 } 295 296 func writeMasked(w io.Writer, s string) io.Writer { 297 buf := make([]byte, 4096) 298 pr, pw := io.Pipe() 299 go func() { 300 for { 301 n, readErr := pr.Read(buf) 302 if readErr != nil && readErr != io.EOF { 303 pr.CloseWithError(readErr) 304 return 305 } 306 var masked string 307 for i := 0; i < n; i++ { 308 masked += s 309 } 310 if _, err := w.Write([]byte(masked)); err != nil { 311 pr.CloseWithError(err) 312 return 313 } 314 if readErr == io.EOF { 315 pr.Close() 316 return 317 } 318 } 319 }() 320 return pw 321 }