go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/butler_test.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package butler
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  
    26  	"google.golang.org/protobuf/types/known/timestamppb"
    27  
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/common/logging"
    30  	"go.chromium.org/luci/common/logging/gologger"
    31  	. "go.chromium.org/luci/common/testing/assertions"
    32  	"go.chromium.org/luci/logdog/api/logpb"
    33  	"go.chromium.org/luci/logdog/client/butler/bootstrap"
    34  	"go.chromium.org/luci/logdog/client/butler/output"
    35  	"go.chromium.org/luci/logdog/client/butler/output/null"
    36  	"go.chromium.org/luci/logdog/client/butlerlib/streamproto"
    37  	"go.chromium.org/luci/logdog/common/types"
    38  
    39  	. "github.com/smartystreets/goconvey/convey"
    40  )
    41  
    42  type testOutput struct {
    43  	sync.Mutex
    44  
    45  	err      error
    46  	maxSize  int
    47  	streams  map[string][]*logpb.LogEntry
    48  	terminal map[string]struct{}
    49  
    50  	closed bool
    51  }
    52  
    53  func (to *testOutput) SendBundle(b *logpb.ButlerLogBundle) error {
    54  	if to.err != nil {
    55  		return to.err
    56  	}
    57  
    58  	to.Lock()
    59  	defer to.Unlock()
    60  
    61  	if to.streams == nil {
    62  		to.streams = map[string][]*logpb.LogEntry{}
    63  		to.terminal = map[string]struct{}{}
    64  	}
    65  	for _, be := range b.Entries {
    66  		name := string(be.Desc.Name)
    67  
    68  		to.streams[name] = append(to.streams[name], be.Logs...)
    69  		if be.TerminalIndex >= 0 {
    70  			to.terminal[name] = struct{}{}
    71  		}
    72  	}
    73  	return nil
    74  }
    75  
    76  func (to *testOutput) MaxSendBundles() int { return 1 }
    77  
    78  func (to *testOutput) MaxSize() int {
    79  	return to.maxSize
    80  }
    81  
    82  func (to *testOutput) Stats() output.Stats {
    83  	return &output.StatsBase{}
    84  }
    85  
    86  func (to *testOutput) URLConstructionEnv() bootstrap.Environment {
    87  	return bootstrap.Environment{}
    88  }
    89  
    90  func (to *testOutput) Close() {
    91  	if to.closed {
    92  		panic("double close")
    93  	}
    94  	to.closed = true
    95  }
    96  
    97  func (to *testOutput) logs(name string) []*logpb.LogEntry {
    98  	to.Lock()
    99  	defer to.Unlock()
   100  
   101  	return to.streams[name]
   102  }
   103  
   104  func (to *testOutput) isTerminal(name string) bool {
   105  	to.Lock()
   106  	defer to.Unlock()
   107  
   108  	_, isTerminal := to.terminal[name]
   109  	return isTerminal
   110  }
   111  
   112  func shouldHaveTextLogs(actual any, expected ...any) string {
   113  	exp := make([]string, len(expected))
   114  	for i, e := range expected {
   115  		exp[i] = e.(string)
   116  	}
   117  
   118  	var lines []string
   119  	var prev string
   120  	for _, le := range actual.([]*logpb.LogEntry) {
   121  		if le.GetText() == nil {
   122  			return fmt.Sprintf("non-text entry: %T %#v", le, le)
   123  		}
   124  
   125  		for _, l := range le.GetText().Lines {
   126  			prev += string(l.Value)
   127  			if l.Delimiter != "" {
   128  				lines = append(lines, prev)
   129  				prev = ""
   130  			}
   131  		}
   132  	}
   133  
   134  	// Add any trailing (non-delimited) data.
   135  	if prev != "" {
   136  		lines = append(lines, prev)
   137  	}
   138  
   139  	return ShouldResemble(lines, exp)
   140  }
   141  
   142  type testStreamData struct {
   143  	data []byte
   144  	err  error
   145  }
   146  
   147  type testStream struct {
   148  	inC     chan *testStreamData
   149  	closedC chan struct{}
   150  
   151  	allowDoubleClose bool
   152  
   153  	desc *logpb.LogStreamDescriptor
   154  }
   155  
   156  func (ts *testStream) data(d []byte, err error) {
   157  	ts.inC <- &testStreamData{
   158  		data: d,
   159  		err:  err,
   160  	}
   161  	if err == io.EOF {
   162  		// If EOF is hit, continue reporting EOF.
   163  		close(ts.inC)
   164  	}
   165  }
   166  
   167  func (ts *testStream) err(err error) {
   168  	ts.data(nil, err)
   169  }
   170  
   171  func (ts *testStream) isClosed() bool {
   172  	select {
   173  	case <-ts.closedC:
   174  		return true
   175  	default:
   176  		return false
   177  	}
   178  }
   179  
   180  func (ts *testStream) Close() error {
   181  	if ts.isClosed() {
   182  		if ts.allowDoubleClose {
   183  			return nil
   184  		}
   185  		panic("double close")
   186  	}
   187  
   188  	close(ts.closedC)
   189  	return nil
   190  }
   191  
   192  func (ts *testStream) Read(b []byte) (int, error) {
   193  	select {
   194  	case <-ts.closedC:
   195  		return 0, io.EOF
   196  
   197  	case d, ok := <-ts.inC:
   198  		// We have data on "inC", but we want closed status to trump this.
   199  		if !ok || ts.isClosed() {
   200  			return 0, io.EOF
   201  		}
   202  		return copy(b, d.data), d.err
   203  	}
   204  }
   205  
   206  type testStreamServer struct {
   207  	err     error
   208  	onNext  func()
   209  	streamC chan *testStream
   210  }
   211  
   212  func newTestStreamServer() *testStreamServer {
   213  	return &testStreamServer{
   214  		streamC: make(chan *testStream),
   215  	}
   216  }
   217  
   218  func (tss *testStreamServer) Address() string { return "test" }
   219  
   220  func (tss *testStreamServer) Listen() error { return tss.err }
   221  
   222  func (tss *testStreamServer) Next() (io.ReadCloser, *logpb.LogStreamDescriptor) {
   223  	if tss.onNext != nil {
   224  		tss.onNext()
   225  	}
   226  
   227  	ts, ok := <-tss.streamC
   228  	if !ok {
   229  		return nil, nil
   230  	}
   231  	return ts, ts.desc
   232  }
   233  
   234  func (tss *testStreamServer) Close() {
   235  	close(tss.streamC)
   236  }
   237  
   238  func (tss *testStreamServer) enqueue(ts *testStream) {
   239  	tss.streamC <- ts
   240  }
   241  
   242  func TestConfig(t *testing.T) {
   243  	t.Parallel()
   244  
   245  	Convey(`A Config instance`, t, func() {
   246  		to := testOutput{
   247  			maxSize: 1024,
   248  		}
   249  		conf := Config{
   250  			Output: &to,
   251  		}
   252  
   253  		Convey(`Will validate.`, func() {
   254  			So(conf.Validate(), ShouldBeNil)
   255  		})
   256  
   257  		Convey(`Will not validate with a nil Output.`, func() {
   258  			conf.Output = nil
   259  			So(conf.Validate(), ShouldErrLike, "an Output must be supplied")
   260  		})
   261  	})
   262  }
   263  
   264  // mkb is shorthand for "make Butler". It calls "new" and panics if there is
   265  // an error.
   266  func mkb(c context.Context, config Config) *Butler {
   267  	b, err := New(c, config)
   268  	if err != nil {
   269  		panic(err)
   270  	}
   271  	return b
   272  }
   273  
   274  func TestButler(t *testing.T) {
   275  	t.Parallel()
   276  
   277  	Convey(`A testing Butler instance`, t, func() {
   278  		c, _ := testclock.UseTime(context.Background(), testclock.TestTimeUTC)
   279  		to := testOutput{
   280  			maxSize: 1024,
   281  		}
   282  
   283  		conf := Config{
   284  			Output:     &to,
   285  			BufferLogs: false,
   286  		}
   287  
   288  		Convey(`Will error if an invalid Config is passed.`, func() {
   289  			conf.Output = nil
   290  			So(conf.Validate(), ShouldNotBeNil)
   291  
   292  			_, err := New(c, conf)
   293  			So(err, ShouldNotBeNil)
   294  		})
   295  
   296  		Convey(`Will Run until Activated, then shut down.`, func() {
   297  			conf.BufferLogs = true // (Coverage)
   298  
   299  			b := mkb(c, conf)
   300  			b.Activate()
   301  			So(b.Wait(), ShouldBeNil)
   302  		})
   303  
   304  		Convey(`Will forward a Shutdown error.`, func() {
   305  			conf.BufferLogs = true // (Coverage)
   306  
   307  			b := mkb(c, conf)
   308  			b.shutdown(errors.New("shutdown error"))
   309  			So(b.Wait(), ShouldErrLike, "shutdown error")
   310  		})
   311  
   312  		Convey(`Will retain the first error and ignore duplicate shutdowns.`, func() {
   313  			b := mkb(c, conf)
   314  			b.shutdown(errors.New("first error"))
   315  			b.shutdown(errors.New("second error"))
   316  			So(b.Wait(), ShouldErrLike, "first error")
   317  		})
   318  
   319  		Convey(`Using a generic stream Properties`, func() {
   320  			newTestStream := func(setup func(d *logpb.LogStreamDescriptor)) *testStream {
   321  				desc := &logpb.LogStreamDescriptor{
   322  					Name:        "test",
   323  					StreamType:  logpb.StreamType_TEXT,
   324  					ContentType: string(types.ContentTypeText),
   325  				}
   326  				if setup != nil {
   327  					setup(desc)
   328  				}
   329  
   330  				return &testStream{
   331  					inC:     make(chan *testStreamData, 16),
   332  					closedC: make(chan struct{}),
   333  					desc:    desc,
   334  				}
   335  			}
   336  
   337  			Convey(`Will not add a stream with an invalid configuration.`, func() {
   338  				// No content type.
   339  				s := newTestStream(func(d *logpb.LogStreamDescriptor) {
   340  					d.ContentType = ""
   341  				})
   342  				b := mkb(c, conf)
   343  				So(b.AddStream(s, s.desc), ShouldNotBeNil)
   344  			})
   345  
   346  			Convey(`Will not add a stream with a duplicate stream name.`, func() {
   347  				b := mkb(c, conf)
   348  
   349  				s0 := newTestStream(nil)
   350  				So(b.AddStream(s0, s0.desc), ShouldBeNil)
   351  
   352  				s1 := newTestStream(nil)
   353  				So(b.AddStream(s1, s1.desc), ShouldErrLike, "duplicate registration")
   354  			})
   355  
   356  			Convey(`Can apply global tags.`, func() {
   357  				conf.GlobalTags = streamproto.TagMap{
   358  					"foo": "bar",
   359  					"baz": "qux",
   360  				}
   361  				desc := &logpb.LogStreamDescriptor{
   362  					Name:        "stdout",
   363  					ContentType: "test/data",
   364  					Timestamp:   timestamppb.New(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)),
   365  				}
   366  
   367  				closeStreams := make(chan *testStreamData)
   368  
   369  				// newTestStream returns a stream that hangs in Read() until
   370  				// 'closeStreams' is closed. We do this to ensure Bundler doesn't
   371  				// "drain" and unregister the stream before GetStreamDescs() calls
   372  				// below have a chance to notice it exists.
   373  				newTestStream := func() io.ReadCloser {
   374  					return &testStream{
   375  						closedC: make(chan struct{}),
   376  						inC:     closeStreams,
   377  					}
   378  				}
   379  
   380  				b := mkb(c, conf)
   381  				defer func() {
   382  					close(closeStreams)
   383  					b.Activate()
   384  					b.Wait()
   385  				}()
   386  
   387  				Convey(`Applies global tags, but allows the stream to override.`, func() {
   388  					desc.Tags = map[string]string{
   389  						"baz": "override",
   390  					}
   391  
   392  					So(b.AddStream(newTestStream(), desc), ShouldBeNil)
   393  					So(b.bundler.GetStreamDescs()["stdout"], ShouldResembleProto, &logpb.LogStreamDescriptor{
   394  						Name:        "stdout",
   395  						ContentType: "test/data",
   396  						Timestamp:   desc.Timestamp,
   397  						Tags: map[string]string{
   398  							"foo": "bar",
   399  							"baz": "override",
   400  						},
   401  					})
   402  				})
   403  
   404  				Convey(`Will apply global tags if the stream has none (nil).`, func() {
   405  					So(b.AddStream(newTestStream(), desc), ShouldBeNil)
   406  					So(b.bundler.GetStreamDescs()["stdout"], ShouldResembleProto, &logpb.LogStreamDescriptor{
   407  						Name:        "stdout",
   408  						ContentType: "test/data",
   409  						Timestamp:   desc.Timestamp,
   410  						Tags: map[string]string{
   411  							"foo": "bar",
   412  							"baz": "qux",
   413  						},
   414  					})
   415  				})
   416  			})
   417  
   418  			Convey(`Run with 256 streams, stream{0..256} will deplete and finish.`, func() {
   419  				b := mkb(c, conf)
   420  				streams := make([]*testStream, 256)
   421  				for i := range streams {
   422  					streams[i] = newTestStream(func(d *logpb.LogStreamDescriptor) {
   423  						d.Name = fmt.Sprintf("stream%d", i)
   424  					})
   425  				}
   426  
   427  				for _, s := range streams {
   428  					So(b.AddStream(s, s.desc), ShouldBeNil)
   429  					s.data([]byte("stream data 0!\n"), nil)
   430  					s.data([]byte("stream data 1!\n"), nil)
   431  				}
   432  
   433  				// Add data to the streams after shutdown.
   434  				for _, s := range streams {
   435  					s.data([]byte("stream data 2!\n"), io.EOF)
   436  				}
   437  
   438  				b.Activate()
   439  				So(b.Wait(), ShouldBeNil)
   440  
   441  				for _, s := range streams {
   442  					name := string(s.desc.Name)
   443  
   444  					So(to.logs(name), shouldHaveTextLogs, "stream data 0!", "stream data 1!", "stream data 2!")
   445  					So(to.isTerminal(name), ShouldBeTrue)
   446  				}
   447  			})
   448  
   449  			Convey(`Shutdown with 256 in-progress streams, stream{0..256} will terminate if they emitted logs.`, func() {
   450  				b := mkb(c, conf)
   451  				streams := make([]*testStream, 256)
   452  				for i := range streams {
   453  					streams[i] = newTestStream(func(d *logpb.LogStreamDescriptor) {
   454  						d.Name = fmt.Sprintf("stream%d", i)
   455  					})
   456  				}
   457  
   458  				for _, s := range streams {
   459  					So(b.AddStream(s, s.desc), ShouldBeNil)
   460  					s.data([]byte("stream data!\n"), nil)
   461  				}
   462  
   463  				b.shutdown(errors.New("test shutdown"))
   464  				So(b.Wait(), ShouldErrLike, "test shutdown")
   465  
   466  				for _, s := range streams {
   467  					if len(to.logs(s.desc.Name)) > 0 {
   468  						So(to.isTerminal(string(s.desc.Name)), ShouldBeTrue)
   469  					} else {
   470  						So(to.isTerminal(string(s.desc.Name)), ShouldBeFalse)
   471  					}
   472  				}
   473  			})
   474  
   475  			Convey(`Using ten test stream servers`, func() {
   476  				servers := make([]*testStreamServer, 10)
   477  				for i := range servers {
   478  					servers[i] = newTestStreamServer()
   479  				}
   480  				streams := []*testStream(nil)
   481  
   482  				Convey(`Can register both before Run and will retain streams.`, func() {
   483  					b := mkb(c, conf)
   484  					for i, tss := range servers {
   485  						b.AddStreamServer(tss)
   486  
   487  						s := newTestStream(func(d *logpb.LogStreamDescriptor) {
   488  							d.Name = fmt.Sprintf("stream%d", i)
   489  						})
   490  						streams = append(streams, s)
   491  						s.data([]byte("test data"), io.EOF)
   492  						tss.enqueue(s)
   493  					}
   494  
   495  					b.Activate()
   496  					So(b.Wait(), ShouldBeNil)
   497  
   498  					for _, s := range streams {
   499  						So(to.logs(s.desc.Name), shouldHaveTextLogs, "test data")
   500  						So(to.isTerminal(s.desc.Name), ShouldBeTrue)
   501  					}
   502  				})
   503  
   504  				Convey(`Can register both during Run and will retain streams.`, func() {
   505  					b := mkb(c, conf)
   506  					for i, tss := range servers {
   507  						b.AddStreamServer(tss)
   508  
   509  						s := newTestStream(func(d *logpb.LogStreamDescriptor) {
   510  							d.Name = fmt.Sprintf("stream%d", i)
   511  						})
   512  						streams = append(streams, s)
   513  						s.data([]byte("test data"), io.EOF)
   514  						tss.enqueue(s)
   515  					}
   516  
   517  					b.Activate()
   518  					So(b.Wait(), ShouldBeNil)
   519  
   520  					for _, s := range streams {
   521  						So(to.logs(s.desc.Name), shouldHaveTextLogs, "test data")
   522  						So(to.isTerminal(s.desc.Name), ShouldBeTrue)
   523  					}
   524  				})
   525  			})
   526  
   527  			Convey(`Will ignore stream registration errors, allowing re-registration.`, func() {
   528  				tss := newTestStreamServer()
   529  
   530  				// Generate an invalid stream for "tss" to register.
   531  				sGood := newTestStream(nil)
   532  				sGood.data([]byte("good test data"), io.EOF)
   533  
   534  				sBad := newTestStream(func(d *logpb.LogStreamDescriptor) {
   535  					d.ContentType = ""
   536  				})
   537  				sBad.data([]byte("bad test data"), io.EOF)
   538  
   539  				b := mkb(c, conf)
   540  				b.AddStreamServer(tss)
   541  				tss.enqueue(sBad)
   542  				tss.enqueue(sGood)
   543  				b.Activate()
   544  				So(b.Wait(), ShouldBeNil)
   545  
   546  				So(sBad.isClosed(), ShouldBeTrue)
   547  				So(sGood.isClosed(), ShouldBeTrue)
   548  				So(to.logs("test"), shouldHaveTextLogs, "good test data")
   549  				So(to.isTerminal("test"), ShouldBeTrue)
   550  			})
   551  		})
   552  
   553  		Convey(`Will terminate if the stream server panics.`, func() {
   554  			tss := newTestStreamServer()
   555  			tss.onNext = func() {
   556  				panic("test panic")
   557  			}
   558  
   559  			b := mkb(c, conf)
   560  			b.AddStreamServer(tss)
   561  			So(b.Wait(), ShouldErrLike, "test panic")
   562  		})
   563  
   564  		Convey(`Can wait for a subset of streams to complete.`, func() {
   565  			c = logging.SetLevel(gologger.StdConfig.Use(c), logging.Debug)
   566  
   567  			b := mkb(c, conf)
   568  			defer func() {
   569  				b.Activate()
   570  				b.Wait()
   571  			}()
   572  
   573  			newTestStream := func(setup func(d *logpb.LogStreamDescriptor)) *testStream {
   574  				desc := &logpb.LogStreamDescriptor{
   575  					Name:        "test",
   576  					StreamType:  logpb.StreamType_TEXT,
   577  					ContentType: string(types.ContentTypeText),
   578  				}
   579  				if setup != nil {
   580  					setup(desc)
   581  				}
   582  
   583  				return &testStream{
   584  					inC:              make(chan *testStreamData, 16),
   585  					closedC:          make(chan struct{}),
   586  					desc:             desc,
   587  					allowDoubleClose: true,
   588  				}
   589  			}
   590  
   591  			deep := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "ns/deep/s" })
   592  			defer deep.Close()
   593  
   594  			ns := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "ns/s" })
   595  			defer ns.Close()
   596  
   597  			s := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "s" })
   598  			defer s.Close()
   599  
   600  			b.AddStream(deep, deep.desc)
   601  			b.AddStream(ns, ns.desc)
   602  			b.AddStream(s, s.desc)
   603  
   604  			wait := func(cvctx C, ns ...types.StreamName) <-chan struct{} {
   605  				ch := make(chan struct{})
   606  				ret := sync.WaitGroup{}
   607  				ret.Add(len(ns))
   608  				go func() {
   609  					ret.Wait()
   610  					close(ch)
   611  				}()
   612  				for _, singleNS := range ns {
   613  					singleNS := singleNS
   614  					go func() {
   615  						defer ret.Done()
   616  						cvctx.So(b.DrainNamespace(c, singleNS), ShouldBeNil)
   617  					}()
   618  				}
   619  				return ch
   620  			}
   621  
   622  			check := func(toWait <-chan struct{}, toClose ...*testStream) {
   623  				select {
   624  				case <-time.After(time.Millisecond):
   625  				case <-toWait:
   626  					panic("we should time out here")
   627  				}
   628  
   629  				for _, c := range toClose {
   630  					So(c.Close(), ShouldBeNil)
   631  				}
   632  				<-toWait
   633  			}
   634  
   635  			Convey(`waiting for already-empty namespace works`, func() {
   636  				So(b.DrainNamespace(c, "other"), ShouldBeNil)
   637  			})
   638  
   639  			Convey(`can wait at a deep level`, func(cvctx C) {
   640  				check(wait(cvctx, "ns/deep"), deep)
   641  
   642  				Convey(`and we now cannot open new streams under that namespace`, func() {
   643  					err := b.AddStream(nil, &logpb.LogStreamDescriptor{
   644  						Name:        "ns/deep/other",
   645  						ContentType: "wat",
   646  					})
   647  					So(err, ShouldErrLike, `namespace "ns/deep/": already closed`)
   648  				})
   649  
   650  				Convey(`can still open new adjacent streams`, func() {
   651  					cool := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "ns/side/other" })
   652  					defer cool.Close()
   653  					So(b.AddStream(cool, cool.desc), ShouldBeNil)
   654  				})
   655  
   656  				Convey(`then we can also wait at higher levels`, func(cvctx C) {
   657  					check(wait(cvctx, "ns"), ns)
   658  				})
   659  			})
   660  
   661  			Convey(`can wait at multiple levels`, func(cvctx C) {
   662  				check(wait(cvctx, "ns", "ns/deep"), ns, deep)
   663  			})
   664  
   665  			Convey(`can cancel the wait and see leftovers`, func() {
   666  				cctx, cancel := context.WithCancel(c)
   667  				cancel()
   668  				So(b.DrainNamespace(cctx, "ns"), ShouldResemble, []types.StreamName{
   669  					"ns/deep/s",
   670  					"ns/s",
   671  				})
   672  			})
   673  
   674  			Convey(`can cancel entire namespace`, func(cvctx C) {
   675  				// TODO(iannucci): We should use this in Wait() for a more orderly
   676  				// shutdown.
   677  				check(wait(cvctx, ""), ns, deep, s)
   678  			})
   679  		})
   680  	})
   681  }
   682  
   683  // Command to run: `go test -cpu 1,4,8  -benchmem -benchtime 5s -bench .`
   684  func BenchmarkButler(b *testing.B) {
   685  	numOfStreams := []int{10, 100, 500}
   686  	testText := []string{
   687  		"This is line one\n",
   688  		"This is another two\n",
   689  		"This is final line",
   690  	}
   691  
   692  	for _, n := range numOfStreams {
   693  		b.Run(fmt.Sprintf("%d-streams", n), func(b *testing.B) {
   694  			for i := 0; i <= b.N; i++ {
   695  				if err := runButlerBenchmark(testText, n); err != nil {
   696  					b.Fatal(err)
   697  				}
   698  			}
   699  		})
   700  	}
   701  }
   702  
   703  // runButlerBenchmark creates a new butler instance and given number of
   704  // streams, then write the supplied text to each stream and wait for
   705  // the butler instance to complete.
   706  func runButlerBenchmark(testText []string, numOfStream int) error {
   707  	conf := Config{
   708  		Output:     &null.Output{},
   709  		BufferLogs: false,
   710  	}
   711  	tb := mkb(context.Background(), conf)
   712  	tss := newTestStreamServer()
   713  	tb.AddStreamServer(tss)
   714  
   715  	testStreams := make([]*testStream, numOfStream)
   716  
   717  	for i := range testStreams {
   718  		testStreams[i] = &testStream{
   719  			inC:     make(chan *testStreamData, 16),
   720  			closedC: make(chan struct{}),
   721  			desc: &logpb.LogStreamDescriptor{
   722  				Name:        fmt.Sprintf("stream-%d", i),
   723  				StreamType:  logpb.StreamType_TEXT,
   724  				ContentType: string(types.ContentTypeText),
   725  			},
   726  		}
   727  		tss.enqueue(testStreams[i])
   728  	}
   729  
   730  	for _, ts := range testStreams {
   731  		go func(s *testStream) {
   732  			for _, line := range testText {
   733  				s.data([]byte(line), nil)
   734  			}
   735  			s.err(io.EOF)
   736  		}(ts)
   737  	}
   738  
   739  	tb.Activate()
   740  	if err := tb.Wait(); err != nil {
   741  		return err
   742  	}
   743  	return nil
   744  }