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  }