go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/common/fetcher/fetcher_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 fetcher
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"io"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/golang/protobuf/proto"
    26  	. "github.com/smartystreets/goconvey/convey"
    27  	"go.chromium.org/luci/common/clock"
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/common/testing/assertions"
    30  	"go.chromium.org/luci/logdog/api/logpb"
    31  	"go.chromium.org/luci/logdog/common/types"
    32  )
    33  
    34  type testSourceCommand struct {
    35  	logEntries []*logpb.LogEntry
    36  
    37  	err   error
    38  	panic bool
    39  
    40  	tidx    types.MessageIndex
    41  	tidxSet bool
    42  }
    43  
    44  // addLogs adds a text log entry for each named indices. The text entry contains
    45  // a single line, "#x", where "x" is the log index.
    46  func (cmd *testSourceCommand) logs(indices ...int64) *testSourceCommand {
    47  	for _, idx := range indices {
    48  		cmd.logEntries = append(cmd.logEntries, &logpb.LogEntry{
    49  			StreamIndex: uint64(idx),
    50  		})
    51  	}
    52  	return cmd
    53  }
    54  
    55  func (cmd *testSourceCommand) terminalIndex(v types.MessageIndex) *testSourceCommand {
    56  	cmd.tidx, cmd.tidxSet = v, true
    57  	return cmd
    58  }
    59  
    60  func (cmd *testSourceCommand) error(err error, panic bool) *testSourceCommand {
    61  	cmd.err, cmd.panic = err, panic
    62  	return cmd
    63  }
    64  
    65  type testSource struct {
    66  	sync.Mutex
    67  
    68  	logs     map[types.MessageIndex]*logpb.LogEntry
    69  	err      error
    70  	panic    bool
    71  	terminal types.MessageIndex
    72  
    73  	history []int
    74  }
    75  
    76  func newTestSource() *testSource {
    77  	return &testSource{
    78  		terminal: -1,
    79  		logs:     make(map[types.MessageIndex]*logpb.LogEntry),
    80  	}
    81  }
    82  
    83  func (ts *testSource) Descriptor() *logpb.LogStreamDescriptor { return nil }
    84  
    85  func (ts *testSource) LogEntries(c context.Context, req *LogRequest) ([]*logpb.LogEntry, types.MessageIndex, error) {
    86  	ts.Lock()
    87  	defer ts.Unlock()
    88  
    89  	if ts.err != nil {
    90  		if ts.panic {
    91  			panic(ts.err)
    92  		}
    93  		return nil, 0, ts.err
    94  	}
    95  
    96  	// We have at least our next log. Build our return value.
    97  	maxCount := req.Count
    98  	if maxCount <= 0 {
    99  		maxCount = len(ts.logs)
   100  	}
   101  	maxBytes := req.Bytes
   102  
   103  	var logs []*logpb.LogEntry
   104  	bytes := int64(0)
   105  	index := req.Index
   106  	for {
   107  		if len(logs) >= maxCount {
   108  			break
   109  		}
   110  
   111  		log, ok := ts.logs[index]
   112  		if !ok {
   113  			break
   114  		}
   115  
   116  		size := int64(5) // We've rigged all logs to have size 5.
   117  		if len(logs) > 0 && maxBytes > 0 && (bytes+size) > maxBytes {
   118  			break
   119  		}
   120  		logs = append(logs, log)
   121  		index++
   122  		bytes += size
   123  	}
   124  	ts.history = append(ts.history, len(logs))
   125  	return logs, ts.terminal, nil
   126  }
   127  
   128  func (ts *testSource) send(cmd *testSourceCommand) {
   129  	ts.Lock()
   130  	defer ts.Unlock()
   131  
   132  	if cmd.err != nil {
   133  		ts.err, ts.panic = cmd.err, cmd.panic
   134  	}
   135  	if cmd.tidxSet {
   136  		ts.terminal = cmd.tidx
   137  	}
   138  	for _, le := range cmd.logEntries {
   139  		ts.logs[types.MessageIndex(le.StreamIndex)] = le
   140  	}
   141  }
   142  
   143  func (ts *testSource) getHistory() []int {
   144  	ts.Lock()
   145  	defer ts.Unlock()
   146  
   147  	h := make([]int, len(ts.history))
   148  	copy(h, ts.history)
   149  	return h
   150  }
   151  
   152  func loadLogs(f *Fetcher, count int) (result []types.MessageIndex, err error) {
   153  	for {
   154  		// Specific limit, hit that limit.
   155  		if count > 0 && len(result) >= count {
   156  			return
   157  		}
   158  
   159  		var le *logpb.LogEntry
   160  		le, err = f.NextLogEntry()
   161  		if le != nil {
   162  			result = append(result, types.MessageIndex(le.StreamIndex))
   163  		}
   164  		if err != nil {
   165  			return
   166  		}
   167  	}
   168  }
   169  
   170  func TestFetcher(t *testing.T) {
   171  	t.Parallel()
   172  
   173  	Convey(`A testing log Source`, t, func() {
   174  		c, tc := testclock.UseTime(context.Background(), testclock.TestTimeLocal)
   175  		c, cancelFunc := context.WithCancel(c)
   176  
   177  		ts := newTestSource()
   178  		o := Options{
   179  			Source: ts,
   180  
   181  			// All message byte sizes will be 5.
   182  			sizeFunc: func(proto.Message) int {
   183  				return 5
   184  			},
   185  		}
   186  
   187  		newFetcher := func() *Fetcher { return New(c, o) }
   188  		reap := func(f *Fetcher) {
   189  			cancelFunc()
   190  			loadLogs(f, 0)
   191  		}
   192  
   193  		Convey(`Uses defaults values when not overridden, and stops when cancelled.`, func() {
   194  			f := newFetcher()
   195  			defer reap(f)
   196  
   197  			So(f.o.BufferCount, ShouldEqual, 0)
   198  			So(f.o.BufferBytes, ShouldEqual, DefaultBufferBytes)
   199  			So(f.o.PrefetchFactor, ShouldEqual, 1)
   200  		})
   201  
   202  		Convey(`With a Count limit of 3.`, func() {
   203  			o.BufferCount = 3
   204  
   205  			Convey(`Will pull 6 sequential log records.`, func() {
   206  				var cmd testSourceCommand
   207  				ts.send(cmd.logs(0, 1, 2, 3, 4, 5).terminalIndex(5))
   208  
   209  				f := newFetcher()
   210  				defer reap(f)
   211  
   212  				logs, err := loadLogs(f, 0)
   213  				So(err, ShouldEqual, io.EOF)
   214  				So(logs, ShouldResemble, []types.MessageIndex{0, 1, 2, 3, 4, 5})
   215  			})
   216  
   217  			Convey(`Will immediately bail out if RequireCompleteStream is set`, func() {
   218  				var cmd testSourceCommand
   219  				ts.send(cmd.logs(0, 1, 2, 3, 4, 5).terminalIndex(-1))
   220  
   221  				o.RequireCompleteStream = true
   222  				f := newFetcher()
   223  				defer reap(f)
   224  
   225  				logs, err := loadLogs(f, 0)
   226  				So(err, ShouldEqual, ErrIncompleteStream)
   227  				So(logs, ShouldBeNil)
   228  			})
   229  
   230  			Convey(`Can read two log records and be cancelled.`, func() {
   231  				var cmd testSourceCommand
   232  				ts.send(cmd.logs(0, 1, 2, 3, 4, 5))
   233  
   234  				f := newFetcher()
   235  				defer reap(f)
   236  
   237  				logs, err := loadLogs(f, 2)
   238  				So(err, ShouldBeNil)
   239  				So(logs, ShouldResemble, []types.MessageIndex{0, 1})
   240  
   241  				cancelFunc()
   242  				_, err = loadLogs(f, 0)
   243  				So(err, ShouldEqual, context.Canceled)
   244  			})
   245  
   246  			Convey(`Will delay for more log records if none are available.`, func() {
   247  				delayed := false
   248  				tc.SetTimerCallback(func(d time.Duration, t clock.Timer) {
   249  					// Add the remaining logs.
   250  					delayed = true
   251  
   252  					var cmd testSourceCommand
   253  					ts.send(cmd.logs(1, 2).terminalIndex(2))
   254  
   255  					tc.Add(d)
   256  				})
   257  
   258  				var cmd testSourceCommand
   259  				ts.send(cmd.logs(0))
   260  
   261  				f := newFetcher()
   262  				defer reap(f)
   263  
   264  				logs, err := loadLogs(f, 1)
   265  				So(err, ShouldBeNil)
   266  				So(logs, ShouldResemble, []types.MessageIndex{0})
   267  
   268  				logs, err = loadLogs(f, 0)
   269  				So(err, ShouldEqual, io.EOF)
   270  				So(logs, ShouldResemble, []types.MessageIndex{1, 2})
   271  				So(delayed, ShouldBeTrue)
   272  			})
   273  
   274  			Convey(`When an error is countered getting the terminal index, returns the error.`, func() {
   275  				var cmd testSourceCommand
   276  				ts.send(cmd.error(errors.New("test error"), false))
   277  
   278  				f := newFetcher()
   279  				defer reap(f)
   280  
   281  				_, err := loadLogs(f, 0)
   282  				So(err, assertions.ShouldErrLike, "test error")
   283  			})
   284  
   285  			Convey(`When an error is countered fetching logs, returns the error.`, func() {
   286  				var cmd testSourceCommand
   287  				ts.send(cmd.logs(0, 1, 2).error(errors.New("test error"), false))
   288  
   289  				f := newFetcher()
   290  				defer reap(f)
   291  
   292  				_, err := loadLogs(f, 0)
   293  				So(err, assertions.ShouldErrLike, "test error")
   294  			})
   295  
   296  			Convey(`If the source panics, it is caught and returned as an error.`, func() {
   297  				var cmd testSourceCommand
   298  				ts.send(cmd.error(errors.New("test error"), true))
   299  
   300  				f := newFetcher()
   301  				defer reap(f)
   302  
   303  				_, err := loadLogs(f, 0)
   304  				So(err, assertions.ShouldErrLike, "panic during fetch")
   305  			})
   306  		})
   307  
   308  		Convey(`With a byte limit of 15`, func() {
   309  			o.BufferBytes = 15
   310  			o.PrefetchFactor = 2
   311  
   312  			var cmd testSourceCommand
   313  			ts.send(cmd.logs(0, 1, 2, 3, 4, 5, 6).terminalIndex(6))
   314  
   315  			f := newFetcher()
   316  			defer reap(f)
   317  
   318  			// First fetch should have asked for 30 bytes (2*15), so 6 logs. After
   319  			// first log was kicked, there is a deficit of one log.
   320  			logs, err := loadLogs(f, 0)
   321  			So(err, ShouldEqual, io.EOF)
   322  			So(logs, ShouldResemble, []types.MessageIndex{0, 1, 2, 3, 4, 5, 6})
   323  
   324  			So(ts.getHistory(), ShouldResemble, []int{6, 1})
   325  		})
   326  
   327  		Convey(`With an index of 1 and a maximum count of 1, fetches exactly 1 log.`, func() {
   328  			o.Index = 1
   329  			o.Count = 1
   330  
   331  			var cmd testSourceCommand
   332  			ts.send(cmd.logs(0, 1, 2, 3, 4, 5, 6).terminalIndex(6))
   333  
   334  			f := newFetcher()
   335  			defer reap(f)
   336  
   337  			// First fetch will ask for exactly one log.
   338  			logs, err := loadLogs(f, 0)
   339  			So(err, ShouldEqual, io.EOF)
   340  			So(logs, ShouldResemble, []types.MessageIndex{1})
   341  
   342  			So(ts.getHistory(), ShouldResemble, []int{1})
   343  		})
   344  	})
   345  }