mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-01 00:23:56 +08:00 
			
		
		
		
	Enable to run build and invoke in background
Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>
This commit is contained in:
		
							
								
								
									
										256
									
								
								util/ioset/ioset.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								util/ioset/ioset.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | ||||
| package ioset | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // Pipe returns a pair of piped readers and writers collection. | ||||
| // They are useful for controlling stdio stream using Forwarder function. | ||||
| func Pipe() (In, Out) { | ||||
| 	r1, w1 := io.Pipe() | ||||
| 	r2, w2 := io.Pipe() | ||||
| 	r3, w3 := io.Pipe() | ||||
| 	return In{r1, w2, w3}, Out{w1, r2, r3} | ||||
| } | ||||
|  | ||||
| type In struct { | ||||
| 	Stdin  io.ReadCloser | ||||
| 	Stdout io.WriteCloser | ||||
| 	Stderr io.WriteCloser | ||||
| } | ||||
|  | ||||
| func (s In) Close() (retErr error) { | ||||
| 	if err := s.Stdin.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	if err := s.Stdout.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	if err := s.Stderr.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type Out struct { | ||||
| 	Stdin  io.WriteCloser | ||||
| 	Stdout io.ReadCloser | ||||
| 	Stderr io.ReadCloser | ||||
| } | ||||
|  | ||||
| func (s Out) Close() (retErr error) { | ||||
| 	if err := s.Stdin.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	if err := s.Stdout.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	if err := s.Stderr.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // Forwarder forwards IO between readers and writers contained | ||||
| // in In and Out structs. | ||||
| // In and Out can be changed during forwarding using SetIn and SetOut methods. | ||||
| type Forwarder struct { | ||||
| 	stdin  *SingleForwarder | ||||
| 	stdout *SingleForwarder | ||||
| 	stderr *SingleForwarder | ||||
| 	mu     sync.Mutex | ||||
|  | ||||
| 	// PropagateStdinClose indicates whether EOF from Stdin of Out should be propagated. | ||||
| 	// If this is true, EOF from Stdin (reader) of Out closes Stdin (writer) of In. | ||||
| 	PropagateStdinClose bool | ||||
| } | ||||
|  | ||||
| func NewForwarder() *Forwarder { | ||||
| 	return &Forwarder{ | ||||
| 		stdin:               NewSingleForwarder(), | ||||
| 		stdout:              NewSingleForwarder(), | ||||
| 		stderr:              NewSingleForwarder(), | ||||
| 		PropagateStdinClose: true, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (f *Forwarder) Close() (retErr error) { | ||||
| 	if err := f.stdin.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	if err := f.stdout.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	if err := f.stderr.Close(); err != nil { | ||||
| 		retErr = err | ||||
| 	} | ||||
| 	return retErr | ||||
| } | ||||
|  | ||||
| func (f *Forwarder) SetOut(out *Out) { | ||||
| 	f.mu.Lock() | ||||
| 	if out == nil { | ||||
| 		f.stdin.SetWriter(nil, func() io.WriteCloser { return nil }) | ||||
| 		f.stdout.SetReader(nil) | ||||
| 		f.stderr.SetReader(nil) | ||||
| 	} else { | ||||
| 		f.stdin.SetWriter(out.Stdin, func() io.WriteCloser { | ||||
| 			if f.PropagateStdinClose { | ||||
| 				out.Stdin.Close() // propagate EOF | ||||
| 				logrus.Debug("forwarder: propagating stdin close") | ||||
| 				return nil | ||||
| 			} | ||||
| 			return out.Stdin | ||||
| 		}) | ||||
| 		f.stdout.SetReader(out.Stdout) | ||||
| 		f.stderr.SetReader(out.Stderr) | ||||
| 	} | ||||
| 	f.mu.Unlock() | ||||
| } | ||||
|  | ||||
| func (f *Forwarder) SetIn(in *In) { | ||||
| 	f.mu.Lock() | ||||
| 	if in == nil { | ||||
| 		f.stdin.SetReader(nil) | ||||
| 		f.stdout.SetWriter(nil, func() io.WriteCloser { return nil }) | ||||
| 		f.stderr.SetWriter(nil, func() io.WriteCloser { return nil }) | ||||
| 	} else { | ||||
| 		f.stdin.SetReader(in.Stdin) | ||||
| 		f.stdout.SetWriter(in.Stdout, func() io.WriteCloser { | ||||
| 			return in.Stdout // continue write; TODO: make it configurable if needed | ||||
| 		}) | ||||
| 		f.stderr.SetWriter(in.Stderr, func() io.WriteCloser { | ||||
| 			return in.Stderr // continue write; TODO: make it configurable if needed | ||||
| 		}) | ||||
| 	} | ||||
| 	f.mu.Unlock() | ||||
| } | ||||
|  | ||||
| // SingleForwarder forwards IO from a reader to a writer. | ||||
| // The reader and writer can be changed during forwarding | ||||
| // using SetReader and SetWriter methods. | ||||
| type SingleForwarder struct { | ||||
| 	curR           io.ReadCloser // closed when set another reader | ||||
| 	curRMu         sync.Mutex | ||||
| 	curW           io.WriteCloser // closed when set another writer | ||||
| 	curWEOFHandler func() io.WriteCloser | ||||
| 	curWMu         sync.Mutex | ||||
|  | ||||
| 	updateRCh chan io.ReadCloser | ||||
| 	doneCh    chan struct{} | ||||
|  | ||||
| 	closeOnce sync.Once | ||||
| } | ||||
|  | ||||
| func NewSingleForwarder() *SingleForwarder { | ||||
| 	f := &SingleForwarder{ | ||||
| 		updateRCh: make(chan io.ReadCloser), | ||||
| 		doneCh:    make(chan struct{}), | ||||
| 	} | ||||
| 	go f.doForward() | ||||
| 	return f | ||||
| } | ||||
|  | ||||
| func (f *SingleForwarder) doForward() { | ||||
| 	var r io.ReadCloser | ||||
| 	for { | ||||
| 		readerInvalid := false | ||||
| 		if r != nil { | ||||
| 			go func() { | ||||
| 				buf := make([]byte, 4096) | ||||
| 				for { | ||||
| 					n, readErr := r.Read(buf) | ||||
| 					if readErr != nil && !errors.Is(readErr, io.EOF) && !errors.Is(readErr, io.ErrClosedPipe) { | ||||
| 						logrus.Debugf("single forwarder: reader error: %v", readErr) | ||||
| 						return | ||||
| 					} | ||||
| 					f.curWMu.Lock() | ||||
| 					w := f.curW | ||||
| 					f.curWMu.Unlock() | ||||
| 					if w != nil { | ||||
| 						if _, err := w.Write(buf[:n]); err != nil && !errors.Is(err, io.ErrClosedPipe) { | ||||
| 							logrus.Debugf("single forwarder: writer error: %v", err) | ||||
| 						} | ||||
| 					} | ||||
| 					if readerInvalid { | ||||
| 						return | ||||
| 					} | ||||
| 					if readErr != io.EOF { | ||||
| 						continue | ||||
| 					} | ||||
|  | ||||
| 					f.curWMu.Lock() | ||||
| 					var newW io.WriteCloser | ||||
| 					if f.curWEOFHandler != nil { | ||||
| 						newW = f.curWEOFHandler() | ||||
| 					} | ||||
| 					f.curW = newW | ||||
| 					f.curWMu.Unlock() | ||||
| 					return | ||||
| 				} | ||||
| 			}() | ||||
| 		} | ||||
| 		select { | ||||
| 		case newR := <-f.updateRCh: | ||||
| 			f.curRMu.Lock() | ||||
| 			if f.curR != nil { | ||||
| 				f.curR.Close() | ||||
| 			} | ||||
| 			f.curR = newR | ||||
| 			r = newR | ||||
| 			readerInvalid = true | ||||
| 			f.curRMu.Unlock() | ||||
| 		case <-f.doneCh: | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Close closes the both of registered reader and writer and finishes the forwarder. | ||||
| func (f *SingleForwarder) Close() (retErr error) { | ||||
| 	f.closeOnce.Do(func() { | ||||
| 		f.curRMu.Lock() | ||||
| 		r := f.curR | ||||
| 		f.curR = nil | ||||
| 		f.curRMu.Unlock() | ||||
| 		if r != nil { | ||||
| 			if err := r.Close(); err != nil { | ||||
| 				retErr = err | ||||
| 			} | ||||
| 		} | ||||
| 		// TODO: Wait until read data fully written to the current writer if needed. | ||||
| 		f.curWMu.Lock() | ||||
| 		w := f.curW | ||||
| 		f.curW = nil | ||||
| 		f.curWMu.Unlock() | ||||
| 		if w != nil { | ||||
| 			if err := w.Close(); err != nil { | ||||
| 				retErr = err | ||||
| 			} | ||||
| 		} | ||||
| 		close(f.doneCh) | ||||
| 	}) | ||||
| 	return retErr | ||||
| } | ||||
|  | ||||
| // SetWriter sets the specified writer as the forward destination. | ||||
| // If curWEOFHandler isn't nil, this will be called when the current reader returns EOF. | ||||
| func (f *SingleForwarder) SetWriter(w io.WriteCloser, curWEOFHandler func() io.WriteCloser) { | ||||
| 	f.curWMu.Lock() | ||||
| 	if f.curW != nil { | ||||
| 		// close all stream on the current IO no to mix with the new IO | ||||
| 		f.curW.Close() | ||||
| 	} | ||||
| 	f.curW = w | ||||
| 	f.curWEOFHandler = curWEOFHandler | ||||
| 	f.curWMu.Unlock() | ||||
| } | ||||
|  | ||||
| // SetWriter sets the specified reader as the forward source. | ||||
| func (f *SingleForwarder) SetReader(r io.ReadCloser) { | ||||
| 	f.updateRCh <- r | ||||
| } | ||||
							
								
								
									
										236
									
								
								util/ioset/mux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								util/ioset/mux.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| package ioset | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| type MuxOut struct { | ||||
| 	Out | ||||
| 	EnableHook  func() | ||||
| 	DisableHook func() | ||||
| } | ||||
|  | ||||
| // NewMuxIO forwards IO stream to/from "in" and "outs". | ||||
| // It toggles IO when it detects "C-a-c" key. | ||||
| // "outs" are closed automatically when "in" reaches EOF. | ||||
| // "in" doesn't closed automatically so the caller needs to explicitly close it. | ||||
| func NewMuxIO(in In, outs []MuxOut, initIdx int, toggleMessage func(prev int, res int) string) *MuxIO { | ||||
| 	m := &MuxIO{ | ||||
| 		enabled:       make(map[int]struct{}), | ||||
| 		in:            in, | ||||
| 		outs:          outs, | ||||
| 		closedCh:      make(chan struct{}), | ||||
| 		toggleMessage: toggleMessage, | ||||
| 	} | ||||
| 	for i := range outs { | ||||
| 		m.enabled[i] = struct{}{} | ||||
| 	} | ||||
| 	m.maxCur = len(outs) | ||||
| 	m.cur = initIdx | ||||
| 	var wg sync.WaitGroup | ||||
| 	var mu sync.Mutex | ||||
| 	for i, o := range outs { | ||||
| 		i, o := i, o | ||||
| 		wg.Add(1) | ||||
| 		go func() { | ||||
| 			defer wg.Done() | ||||
| 			if err := copyToFunc(o.Stdout, func() (io.Writer, error) { | ||||
| 				if m.cur == i { | ||||
| 					return in.Stdout, nil | ||||
| 				} | ||||
| 				return nil, nil | ||||
| 			}); err != nil { | ||||
| 				logrus.WithField("output index", i).WithError(err).Warnf("failed to write stdout") | ||||
| 			} | ||||
| 			if err := o.Stdout.Close(); err != nil { | ||||
| 				logrus.WithField("output index", i).WithError(err).Warnf("failed to close stdout") | ||||
| 			} | ||||
| 		}() | ||||
| 		wg.Add(1) | ||||
| 		go func() { | ||||
| 			defer wg.Done() | ||||
| 			if err := copyToFunc(o.Stderr, func() (io.Writer, error) { | ||||
| 				if m.cur == i { | ||||
| 					return in.Stderr, nil | ||||
| 				} | ||||
| 				return nil, nil | ||||
| 			}); err != nil { | ||||
| 				logrus.WithField("output index", i).WithError(err).Warnf("failed to write stderr") | ||||
| 			} | ||||
| 			if err := o.Stderr.Close(); err != nil { | ||||
| 				logrus.WithField("output index", i).WithError(err).Warnf("failed to close stderr") | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| 	go func() { | ||||
| 		errToggle := errors.Errorf("toggle IO") | ||||
| 		for { | ||||
| 			prevIsControlSequence := false | ||||
| 			if err := copyToFunc(traceReader(in.Stdin, func(r rune) (bool, error) { | ||||
| 				// Toggle IO when it detects C-a-c | ||||
| 				// TODO: make it configurable if needed | ||||
| 				if int(r) == 1 { | ||||
| 					prevIsControlSequence = true | ||||
| 					return false, nil | ||||
| 				} | ||||
| 				defer func() { prevIsControlSequence = false }() | ||||
| 				if prevIsControlSequence { | ||||
| 					if string(r) == "c" { | ||||
| 						return false, errToggle | ||||
| 					} | ||||
| 				} | ||||
| 				return true, nil | ||||
| 			}), func() (io.Writer, error) { | ||||
| 				mu.Lock() | ||||
| 				o := outs[m.cur] | ||||
| 				mu.Unlock() | ||||
| 				return o.Stdin, nil | ||||
| 			}); !errors.Is(err, errToggle) { | ||||
| 				if err != nil { | ||||
| 					logrus.WithError(err).Warnf("failed to read stdin") | ||||
| 				} | ||||
| 				break | ||||
| 			} | ||||
| 			m.toggleIO() | ||||
| 		} | ||||
|  | ||||
| 		// propagate stdin EOF | ||||
| 		for i, o := range outs { | ||||
| 			if err := o.Stdin.Close(); err != nil { | ||||
| 				logrus.WithError(err).Warnf("failed to close stdin of %d", i) | ||||
| 			} | ||||
| 		} | ||||
| 		wg.Wait() | ||||
| 		close(m.closedCh) | ||||
| 	}() | ||||
| 	return m | ||||
| } | ||||
|  | ||||
| type MuxIO struct { | ||||
| 	cur           int | ||||
| 	maxCur        int | ||||
| 	enabled       map[int]struct{} | ||||
| 	mu            sync.Mutex | ||||
| 	in            In | ||||
| 	outs          []MuxOut | ||||
| 	closedCh      chan struct{} | ||||
| 	toggleMessage func(prev int, res int) string | ||||
| } | ||||
|  | ||||
| func (m *MuxIO) waitClosed() { | ||||
| 	<-m.closedCh | ||||
| } | ||||
|  | ||||
| func (m *MuxIO) Enable(i int) { | ||||
| 	m.mu.Lock() | ||||
| 	defer m.mu.Unlock() | ||||
| 	m.enabled[i] = struct{}{} | ||||
| } | ||||
|  | ||||
| func (m *MuxIO) Disable(i int) error { | ||||
| 	m.mu.Lock() | ||||
| 	defer m.mu.Unlock() | ||||
| 	if i == 0 { | ||||
| 		return errors.Errorf("disabling 0th io is prohibited") | ||||
| 	} | ||||
| 	delete(m.enabled, i) | ||||
| 	if m.cur == i { | ||||
| 		m.toggleIO() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MuxIO) toggleIO() { | ||||
| 	if m.outs[m.cur].DisableHook != nil { | ||||
| 		m.outs[m.cur].DisableHook() | ||||
| 	} | ||||
| 	prev := m.cur | ||||
| 	for { | ||||
| 		if m.cur+1 >= m.maxCur { | ||||
| 			m.cur = 0 | ||||
| 		} else { | ||||
| 			m.cur++ | ||||
| 		} | ||||
| 		if _, ok := m.enabled[m.cur]; !ok { | ||||
| 			continue | ||||
| 		} | ||||
| 		break | ||||
| 	} | ||||
| 	res := m.cur | ||||
| 	if m.outs[m.cur].EnableHook != nil { | ||||
| 		m.outs[m.cur].EnableHook() | ||||
| 	} | ||||
| 	fmt.Fprint(m.in.Stdout, m.toggleMessage(prev, res)) | ||||
| } | ||||
|  | ||||
| func traceReader(r io.ReadCloser, f func(rune) (bool, error)) io.ReadCloser { | ||||
| 	pr, pw := io.Pipe() | ||||
| 	go func() { | ||||
| 		br := bufio.NewReader(r) | ||||
| 		for { | ||||
| 			rn, _, err := br.ReadRune() | ||||
| 			if err != nil { | ||||
| 				if err == io.EOF { | ||||
| 					pw.Close() | ||||
| 					return | ||||
| 				} | ||||
| 				pw.CloseWithError(err) | ||||
| 				return | ||||
| 			} | ||||
| 			if isWrite, err := f(rn); err != nil { | ||||
| 				pw.CloseWithError(err) | ||||
| 				return | ||||
| 			} else if !isWrite { | ||||
| 				continue | ||||
| 			} | ||||
| 			if _, err := pw.Write([]byte(string(rn))); err != nil { | ||||
| 				pw.CloseWithError(err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	return &readerWithClose{ | ||||
| 		Reader: pr, | ||||
| 		closeFunc: func() error { | ||||
| 			pr.Close() | ||||
| 			return r.Close() | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func copyToFunc(r io.Reader, wFunc func() (io.Writer, error)) error { | ||||
| 	buf := make([]byte, 4096) | ||||
| 	for { | ||||
| 		n, readErr := r.Read(buf) | ||||
| 		if readErr != nil && readErr != io.EOF { | ||||
| 			return readErr | ||||
| 		} | ||||
| 		w, err := wFunc() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if w != nil { | ||||
| 			if _, err := w.Write(buf[:n]); err != nil { | ||||
| 				logrus.WithError(err).Debugf("failed to copy") | ||||
| 			} | ||||
| 		} | ||||
| 		if readErr == io.EOF { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type readerWithClose struct { | ||||
| 	io.Reader | ||||
| 	closeFunc func() error | ||||
| } | ||||
|  | ||||
| func (r *readerWithClose) Close() error { | ||||
| 	return r.closeFunc() | ||||
| } | ||||
							
								
								
									
										321
									
								
								util/ioset/mux_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								util/ioset/mux_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | ||||
| package ioset | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"golang.org/x/sync/errgroup" | ||||
| ) | ||||
|  | ||||
| // TestMuxIO tests MuxIO | ||||
| func TestMuxIO(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name       string | ||||
| 		inputs     []instruction | ||||
| 		initIdx    int | ||||
| 		outputsNum int | ||||
| 		wants      []string | ||||
|  | ||||
| 		// Everytime string is written to the mux stdin, the output end | ||||
| 		// that received the string write backs to the string that is masked with | ||||
| 		// its index number. This is useful to check if writeback is written from the | ||||
| 		// expected output destination. | ||||
| 		wantsMaskedOutput string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "single output", | ||||
| 			inputs: []instruction{ | ||||
| 				input("foo\nbar\n"), | ||||
| 				toggle(), | ||||
| 				input("1234"), | ||||
| 				toggle(), | ||||
| 				input("456"), | ||||
| 			}, | ||||
| 			initIdx:           0, | ||||
| 			outputsNum:        1, | ||||
| 			wants:             []string{"foo\nbar\n1234456"}, | ||||
| 			wantsMaskedOutput: `^0+$`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "multi output", | ||||
| 			inputs: []instruction{ | ||||
| 				input("foo\nbar\n"), | ||||
| 				toggle(), | ||||
| 				input("12" + string([]rune{rune(1)}) + "34abc"), | ||||
| 				toggle(), | ||||
| 				input("456"), | ||||
| 			}, | ||||
| 			initIdx:           0, | ||||
| 			outputsNum:        3, | ||||
| 			wants:             []string{"foo\nbar\n", "1234abc", "456"}, | ||||
| 			wantsMaskedOutput: `^0+1+2+$`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "multi output with nonzero index", | ||||
| 			inputs: []instruction{ | ||||
| 				input("foo\nbar\n"), | ||||
| 				toggle(), | ||||
| 				input("1234"), | ||||
| 				toggle(), | ||||
| 				input("456"), | ||||
| 			}, | ||||
| 			initIdx:           1, | ||||
| 			outputsNum:        3, | ||||
| 			wants:             []string{"456", "foo\nbar\n", "1234"}, | ||||
| 			wantsMaskedOutput: `^1+2+0+$`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "multi output many toggles", | ||||
| 			inputs: []instruction{ | ||||
| 				input("foo\nbar\n"), | ||||
| 				toggle(), | ||||
| 				input("1234"), | ||||
| 				toggle(), | ||||
| 				toggle(), | ||||
| 				input("456"), | ||||
| 				toggle(), | ||||
| 				input("%%%%"), | ||||
| 				toggle(), | ||||
| 				toggle(), | ||||
| 				toggle(), | ||||
| 				input("aaaa"), | ||||
| 			}, | ||||
| 			initIdx:           0, | ||||
| 			outputsNum:        3, | ||||
| 			wants:             []string{"foo\nbar\n456", "1234%%%%aaaa", ""}, | ||||
| 			wantsMaskedOutput: `^0+1+0+1+$`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "enable disable", | ||||
| 			inputs: []instruction{ | ||||
| 				input("foo\nbar\n"), | ||||
| 				toggle(), | ||||
| 				input("1234"), | ||||
| 				toggle(), | ||||
| 				input("456"), | ||||
| 				disable(2), | ||||
| 				input("%%%%"), | ||||
| 				enable(2), | ||||
| 				toggle(), | ||||
| 				toggle(), | ||||
| 				input("aaa"), | ||||
| 				disable(2), | ||||
| 				disable(1), | ||||
| 				input("1111"), | ||||
| 				toggle(), | ||||
| 				input("2222"), | ||||
| 				toggle(), | ||||
| 				input("3333"), | ||||
| 			}, | ||||
| 			initIdx:           0, | ||||
| 			outputsNum:        3, | ||||
| 			wants:             []string{"foo\nbar\n%%%%111122223333", "1234", "456aaa"}, | ||||
| 			wantsMaskedOutput: `^0+1+2+0+2+0+$`, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			inBuf, end, in := newTestIn(t) | ||||
| 			var outBufs []*outBuf | ||||
| 			var outs []MuxOut | ||||
| 			if tt.outputsNum != len(tt.wants) { | ||||
| 				t.Fatalf("wants != outputsNum") | ||||
| 			} | ||||
| 			for i := 0; i < tt.outputsNum; i++ { | ||||
| 				outBuf, out := newTestOut(t, i) | ||||
| 				outBufs = append(outBufs, outBuf) | ||||
| 				outs = append(outs, MuxOut{out, nil, nil}) | ||||
| 			} | ||||
| 			mio := NewMuxIO(in, outs, tt.initIdx, func(prev int, res int) string { return "" }) | ||||
| 			for _, i := range tt.inputs { | ||||
| 				// Add input to MuxIO | ||||
| 				istr, writeback := i(mio) | ||||
| 				if _, err := end.Stdin.Write([]byte(istr)); err != nil { | ||||
| 					t.Fatalf("failed to write data to stdin: %v", err) | ||||
| 				} | ||||
|  | ||||
| 				// Wait for writeback of this input | ||||
| 				var eg errgroup.Group | ||||
| 				eg.Go(func() error { | ||||
| 					outbuf := make([]byte, len(writeback)) | ||||
| 					if _, err := io.ReadAtLeast(end.Stdout, outbuf, len(outbuf)); err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 					return nil | ||||
| 				}) | ||||
| 				eg.Go(func() error { | ||||
| 					errbuf := make([]byte, len(writeback)) | ||||
| 					if _, err := io.ReadAtLeast(end.Stderr, errbuf, len(errbuf)); err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 					return nil | ||||
| 				}) | ||||
| 				if err := eg.Wait(); err != nil { | ||||
| 					t.Fatalf("failed to wait for output: %v", err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Close stdin on this MuxIO | ||||
| 			end.Stdin.Close() | ||||
|  | ||||
| 			// Wait for all output ends reach EOF | ||||
| 			mio.waitClosed() | ||||
|  | ||||
| 			// Close stdout/stderr as well | ||||
| 			in.Close() | ||||
|  | ||||
| 			// Check if each output end received expected string | ||||
| 			<-inBuf.doneCh | ||||
| 			for i, o := range outBufs { | ||||
| 				<-o.doneCh | ||||
| 				if o.stdin != tt.wants[i] { | ||||
| 					t.Fatalf("output[%d]: got %q; wanted %q", i, o.stdin, tt.wants[i]) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Check if expected string is returned from expected outputs | ||||
| 			if !regexp.MustCompile(tt.wantsMaskedOutput).MatchString(inBuf.stdout) { | ||||
| 				t.Fatalf("stdout: got %q; wanted %q", inBuf.stdout, tt.wantsMaskedOutput) | ||||
| 			} | ||||
| 			if !regexp.MustCompile(tt.wantsMaskedOutput).MatchString(inBuf.stderr) { | ||||
| 				t.Fatalf("stderr: got %q; wanted %q", inBuf.stderr, tt.wantsMaskedOutput) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type instruction func(m *MuxIO) (intput string, writeBackView string) | ||||
|  | ||||
| func input(s string) instruction { | ||||
| 	return func(m *MuxIO) (string, string) { | ||||
| 		return s, strings.ReplaceAll(s, string([]rune{rune(1)}), "") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func toggle() instruction { | ||||
| 	return func(m *MuxIO) (string, string) { | ||||
| 		return string([]rune{rune(1)}) + "c", "" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func enable(i int) instruction { | ||||
| 	return func(m *MuxIO) (string, string) { | ||||
| 		m.Enable(i) | ||||
| 		return "", "" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func disable(i int) instruction { | ||||
| 	return func(m *MuxIO) (string, string) { | ||||
| 		m.Disable(i) | ||||
| 		return "", "" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type inBuf struct { | ||||
| 	stdout string | ||||
| 	stderr string | ||||
| 	doneCh chan struct{} | ||||
| } | ||||
|  | ||||
| func newTestIn(t *testing.T) (*inBuf, Out, In) { | ||||
| 	ti := &inBuf{ | ||||
| 		doneCh: make(chan struct{}), | ||||
| 	} | ||||
| 	gotOutR, gotOutW := io.Pipe() | ||||
| 	gotErrR, gotErrW := io.Pipe() | ||||
| 	outR, outW := io.Pipe() | ||||
| 	var eg errgroup.Group | ||||
| 	eg.Go(func() error { | ||||
| 		buf := new(bytes.Buffer) | ||||
| 		if _, err := io.Copy(io.MultiWriter(gotOutW, buf), outR); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		ti.stdout = buf.String() | ||||
| 		return nil | ||||
| 	}) | ||||
| 	errR, errW := io.Pipe() | ||||
| 	eg.Go(func() error { | ||||
| 		buf := new(bytes.Buffer) | ||||
| 		if _, err := io.Copy(io.MultiWriter(gotErrW, buf), errR); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		ti.stderr = buf.String() | ||||
| 		return nil | ||||
| 	}) | ||||
| 	go func() { | ||||
| 		eg.Wait() | ||||
| 		close(ti.doneCh) | ||||
| 	}() | ||||
| 	inR, inW := io.Pipe() | ||||
| 	return ti, Out{Stdin: inW, Stdout: gotOutR, Stderr: gotErrR}, In{Stdin: inR, Stdout: outW, Stderr: errW} | ||||
| } | ||||
|  | ||||
| type outBuf struct { | ||||
| 	idx    int | ||||
| 	stdin  string | ||||
| 	doneCh chan struct{} | ||||
| } | ||||
|  | ||||
| func newTestOut(t *testing.T, idx int) (*outBuf, Out) { | ||||
| 	to := &outBuf{ | ||||
| 		idx:    idx, | ||||
| 		doneCh: make(chan struct{}), | ||||
| 	} | ||||
| 	inR, inW := io.Pipe() | ||||
| 	outR, outW := io.Pipe() | ||||
| 	errR, errW := io.Pipe() | ||||
| 	go func() { | ||||
| 		defer inR.Close() | ||||
| 		defer outW.Close() | ||||
| 		defer errW.Close() | ||||
| 		buf := new(bytes.Buffer) | ||||
| 		mw := io.MultiWriter(buf, | ||||
| 			writeMasked(outW, fmt.Sprintf("%d", to.idx)), | ||||
| 			writeMasked(errW, fmt.Sprintf("%d", to.idx)), | ||||
| 		) | ||||
| 		if _, err := io.Copy(mw, inR); err != nil { | ||||
| 			inR.CloseWithError(err) | ||||
| 			outW.CloseWithError(err) | ||||
| 			errW.CloseWithError(err) | ||||
| 			return | ||||
| 		} | ||||
| 		to.stdin = string(buf.Bytes()) | ||||
| 		outW.Close() | ||||
| 		errW.Close() | ||||
| 		close(to.doneCh) | ||||
| 	}() | ||||
| 	return to, Out{Stdin: inW, Stdout: outR, Stderr: errR} | ||||
| } | ||||
|  | ||||
| func writeMasked(w io.Writer, s string) io.Writer { | ||||
| 	buf := make([]byte, 4096) | ||||
| 	pr, pw := io.Pipe() | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			n, readErr := pr.Read(buf) | ||||
| 			if readErr != nil && readErr != io.EOF { | ||||
| 				pr.CloseWithError(readErr) | ||||
| 				return | ||||
| 			} | ||||
| 			var masked string | ||||
| 			for i := 0; i < n; i++ { | ||||
| 				masked += s | ||||
| 			} | ||||
| 			if _, err := w.Write([]byte(masked)); err != nil { | ||||
| 				pr.CloseWithError(err) | ||||
| 				return | ||||
| 			} | ||||
| 			if readErr == io.EOF { | ||||
| 				pr.Close() | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	return pw | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Kohei Tokunaga
					Kohei Tokunaga