go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/common/storage/archive/storage_test.go (about)

     1  // Copyright 2016 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 archive
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"testing"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/gcloud/gs"
    26  	"go.chromium.org/luci/logdog/api/logpb"
    27  	"go.chromium.org/luci/logdog/common/archive"
    28  	"go.chromium.org/luci/logdog/common/renderer"
    29  	"go.chromium.org/luci/logdog/common/storage"
    30  	"go.chromium.org/luci/logdog/common/storage/memory"
    31  
    32  	cloudStorage "cloud.google.com/go/storage"
    33  	"google.golang.org/protobuf/proto"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  
    37  	. "go.chromium.org/luci/common/testing/assertions"
    38  )
    39  
    40  const (
    41  	testIndexPath  = gs.Path("gs://+/index")
    42  	testStreamPath = gs.Path("gs://+/stream")
    43  )
    44  
    45  type logStreamGenerator struct {
    46  	lines []string
    47  
    48  	indexBuf  bytes.Buffer
    49  	streamBuf bytes.Buffer
    50  }
    51  
    52  func (g *logStreamGenerator) lineFromEntry(e *storage.Entry) string {
    53  	le, err := e.GetLogEntry()
    54  	if err != nil {
    55  		panic(err)
    56  	}
    57  
    58  	text := le.GetText()
    59  	if text == nil || len(text.Lines) != 1 {
    60  		panic(fmt.Errorf("bad generated log entry: %#v", le))
    61  	}
    62  	return string(text.Lines[0].Value)
    63  }
    64  
    65  func (g *logStreamGenerator) generate(lines ...string) {
    66  	logEntries := make([]*logpb.LogEntry, len(lines))
    67  	for i, line := range lines {
    68  		logEntries[i] = &logpb.LogEntry{
    69  			PrefixIndex: uint64(i),
    70  			StreamIndex: uint64(i),
    71  			Content: &logpb.LogEntry_Text{
    72  				Text: &logpb.Text{
    73  					Lines: []*logpb.Text_Line{
    74  						{Value: []byte(line), Delimiter: "\n"},
    75  					},
    76  				},
    77  			},
    78  		}
    79  	}
    80  
    81  	g.lines = lines
    82  	g.indexBuf.Reset()
    83  	g.streamBuf.Reset()
    84  	src := renderer.StaticSource(logEntries)
    85  	err := archive.Archive(archive.Manifest{
    86  		Desc: &logpb.LogStreamDescriptor{
    87  			Prefix: "prefix",
    88  			Name:   "name",
    89  		},
    90  		Source:      &src,
    91  		LogWriter:   &g.streamBuf,
    92  		IndexWriter: &g.indexBuf,
    93  	})
    94  	if err != nil {
    95  		panic(err)
    96  	}
    97  }
    98  
    99  func (g *logStreamGenerator) pruneIndexHints() {
   100  	g.modIndex(func(idx *logpb.LogIndex) {
   101  		idx.LastPrefixIndex = 0
   102  		idx.LastStreamIndex = 0
   103  		idx.LogEntryCount = 0
   104  	})
   105  }
   106  
   107  func (g *logStreamGenerator) sparseIndex(indices ...uint64) {
   108  	idxMap := make(map[uint64]struct{}, len(indices))
   109  	for _, i := range indices {
   110  		idxMap[i] = struct{}{}
   111  	}
   112  
   113  	g.modIndex(func(idx *logpb.LogIndex) {
   114  		entries := make([]*logpb.LogIndex_Entry, 0, len(idx.Entries))
   115  		for _, entry := range idx.Entries {
   116  			if _, ok := idxMap[entry.StreamIndex]; ok {
   117  				entries = append(entries, entry)
   118  			}
   119  		}
   120  		idx.Entries = entries
   121  	})
   122  }
   123  
   124  func (g *logStreamGenerator) modIndex(fn func(*logpb.LogIndex)) {
   125  	var index logpb.LogIndex
   126  	if err := proto.Unmarshal(g.indexBuf.Bytes(), &index); err != nil {
   127  		panic(err)
   128  	}
   129  	fn(&index)
   130  	data, err := proto.Marshal(&index)
   131  	if err != nil {
   132  		panic(err)
   133  	}
   134  	g.indexBuf.Reset()
   135  	if _, err := g.indexBuf.Write(data); err != nil {
   136  		panic(err)
   137  	}
   138  }
   139  
   140  type errReader struct {
   141  	io.Reader
   142  	err error
   143  }
   144  
   145  func (r *errReader) Read(d []byte) (int, error) {
   146  	if r.err != nil {
   147  		return 0, r.err
   148  	}
   149  	return r.Reader.Read(d)
   150  }
   151  
   152  type fakeGSClient struct {
   153  	gs.Client
   154  
   155  	index  []byte
   156  	stream []byte
   157  
   158  	closed bool
   159  
   160  	err       error
   161  	indexErr  error
   162  	streamErr error
   163  }
   164  
   165  func (c *fakeGSClient) assertNotClosed() {
   166  	if c.closed {
   167  		panic(errors.New("client is closed"))
   168  	}
   169  }
   170  
   171  func (c *fakeGSClient) load(g *logStreamGenerator) {
   172  	c.index = append([]byte{}, g.indexBuf.Bytes()...)
   173  	c.stream = append([]byte{}, g.streamBuf.Bytes()...)
   174  }
   175  
   176  func (c *fakeGSClient) Close() error {
   177  	c.assertNotClosed()
   178  	c.closed = true
   179  	return nil
   180  }
   181  
   182  func (c *fakeGSClient) NewReader(p gs.Path, offset, length int64) (io.ReadCloser, error) {
   183  	c.assertNotClosed()
   184  
   185  	// If we have a client-level error, return it.
   186  	if c.err != nil {
   187  		return nil, c.err
   188  	}
   189  
   190  	var (
   191  		data      []byte
   192  		readerErr error
   193  	)
   194  	switch p {
   195  	case testIndexPath:
   196  		data, readerErr = c.index, c.indexErr
   197  	case testStreamPath:
   198  		data, readerErr = c.stream, c.streamErr
   199  	default:
   200  		return nil, cloudStorage.ErrObjectNotExist
   201  	}
   202  
   203  	if offset >= 0 {
   204  		if offset >= int64(len(data)) {
   205  			offset = int64(len(data))
   206  		}
   207  		data = data[offset:]
   208  	}
   209  
   210  	if length >= 0 {
   211  		if length > int64(len(data)) {
   212  			length = int64(len(data))
   213  		}
   214  		data = data[:length]
   215  	}
   216  	return io.NopCloser(&errReader{bytes.NewReader(data), readerErr}), nil
   217  }
   218  
   219  func testArchiveStorage(t *testing.T, limit int64) {
   220  	Convey(`A testing archive instance`, t, func() {
   221  		var (
   222  			c      = context.Background()
   223  			client fakeGSClient
   224  			gen    logStreamGenerator
   225  		)
   226  		defer client.Close()
   227  
   228  		opts := Options{
   229  			Index:  testIndexPath,
   230  			Stream: testStreamPath,
   231  			Client: &client,
   232  		}
   233  		if limit > 0 {
   234  			opts.Client = &gs.LimitedClient{
   235  				Client:       opts.Client,
   236  				MaxReadBytes: limit,
   237  			}
   238  		}
   239  
   240  		st, err := New(opts)
   241  		So(err, ShouldBeNil)
   242  		defer st.Close()
   243  
   244  		stImpl := st.(*storageImpl)
   245  
   246  		Convey(`Will fail to Put with ErrReadOnly`, func() {
   247  			So(st.Put(c, storage.PutRequest{}), ShouldEqual, storage.ErrReadOnly)
   248  		})
   249  
   250  		Convey(`Given a stream with 5 log entries`, func() {
   251  			gen.generate("foo", "bar", "baz", "qux", "quux")
   252  
   253  			// Basic test cases.
   254  			for _, tc := range []struct {
   255  				title string
   256  				mod   func()
   257  			}{
   258  				{`Complete index`, func() {}},
   259  				{`Empty index protobuf`, func() { gen.sparseIndex() }},
   260  				{`No index provided`, func() { stImpl.Index = "" }},
   261  				{`Invalid index path`, func() { stImpl.Index = "does-not-exist" }},
   262  				{`Sparse index with a start and terminal entry`, func() { gen.sparseIndex(0, 2, 4) }},
   263  				{`Sparse index with a terminal entry`, func() { gen.sparseIndex(1, 3, 4) }},
   264  				{`Sparse index missing a terminal entry`, func() { gen.sparseIndex(1, 3) }},
   265  			} {
   266  				Convey(fmt.Sprintf(`Test Case: %q`, tc.title), func() {
   267  					tc.mod()
   268  
   269  					// Run through per-testcase variant set.
   270  					for _, variant := range []struct {
   271  						title string
   272  						mod   func()
   273  					}{
   274  						{"with hints", func() {}},
   275  						{"without hints", func() { gen.pruneIndexHints() }},
   276  					} {
   277  						Convey(variant.title, func() {
   278  							variant.mod()
   279  							client.load(&gen)
   280  
   281  							var entries []string
   282  							collect := func(e *storage.Entry) bool {
   283  								entries = append(entries, gen.lineFromEntry(e))
   284  								return true
   285  							}
   286  
   287  							Convey(`Can Get [0..]`, func() {
   288  								So(st.Get(c, storage.GetRequest{}, collect), ShouldBeNil)
   289  								So(entries, ShouldResemble, gen.lines)
   290  							})
   291  
   292  							Convey(`Can Get [1..].`, func() {
   293  								So(st.Get(c, storage.GetRequest{Index: 1}, collect), ShouldBeNil)
   294  								So(entries, ShouldResemble, gen.lines[1:])
   295  							})
   296  
   297  							Convey(`Can Get [1..2].`, func() {
   298  								So(st.Get(c, storage.GetRequest{Index: 1, Limit: 2}, collect), ShouldBeNil)
   299  								So(entries, ShouldResemble, gen.lines[1:3])
   300  							})
   301  
   302  							Convey(`Can Get [5..].`, func() {
   303  								So(st.Get(c, storage.GetRequest{Index: 5}, collect), ShouldBeNil)
   304  								So(entries, ShouldHaveLength, 0)
   305  							})
   306  
   307  							Convey(`Can Get [4].`, func() {
   308  								So(st.Get(c, storage.GetRequest{Index: 4, Limit: 1}, collect), ShouldBeNil)
   309  								So(entries, ShouldResemble, gen.lines[4:])
   310  							})
   311  
   312  							Convey(`Can tail.`, func() {
   313  								e, err := st.Tail(c, "", "")
   314  								So(err, ShouldBeNil)
   315  								So(gen.lineFromEntry(e), ShouldEqual, gen.lines[len(gen.lines)-1])
   316  							})
   317  						})
   318  					}
   319  				})
   320  			}
   321  		})
   322  
   323  		// Individual error test cases.
   324  		for _, tc := range []struct {
   325  			title string
   326  			fn    func() error
   327  		}{
   328  			{"Get", func() error { return st.Get(c, storage.GetRequest{}, func(*storage.Entry) bool { return true }) }},
   329  			{"Tail", func() (err error) {
   330  				_, err = st.Tail(c, "", "")
   331  				return
   332  			}},
   333  		} {
   334  			Convey(fmt.Sprintf("Testing retrieval: %q", tc.title), func() {
   335  				Convey(`With missing log stream returns ErrDoesNotExist.`, func() {
   336  					stImpl.Stream = "does-not-exist"
   337  
   338  					So(st.Get(c, storage.GetRequest{}, nil), ShouldEqual, storage.ErrDoesNotExist)
   339  				})
   340  
   341  				Convey(`With a client error returns that error.`, func() {
   342  					client.err = errors.New("test error")
   343  
   344  					So(errors.Unwrap(tc.fn()), ShouldEqual, client.err)
   345  				})
   346  
   347  				Convey(`With an index reader error returns that error.`, func() {
   348  					client.indexErr = errors.New("test error")
   349  
   350  					So(errors.Unwrap(tc.fn()), ShouldEqual, client.indexErr)
   351  				})
   352  
   353  				Convey(`With an stream reader error returns that error.`, func() {
   354  					client.streamErr = errors.New("test error")
   355  
   356  					So(errors.Unwrap(tc.fn()), ShouldEqual, client.streamErr)
   357  				})
   358  
   359  				Convey(`With junk index data returns an error.`, func() {
   360  					client.index = []byte{0x00}
   361  
   362  					So(tc.fn(), ShouldErrLike, "failed to unmarshal index")
   363  				})
   364  
   365  				Convey(`With junk stream data returns an error.`, func() {
   366  					client.stream = []byte{0x00, 0x01, 0xff}
   367  
   368  					So(tc.fn(), ShouldErrLike, "failed to unmarshal")
   369  				})
   370  
   371  				Convey(`With data entries and a cache, only loads the index once.`, func() {
   372  					var cache memory.Cache
   373  					stImpl.Cache = &cache
   374  
   375  					gen.generate("foo", "bar", "baz", "qux", "quux")
   376  					client.load(&gen)
   377  
   378  					// Assert that an attempted load will fail with an error. This is so
   379  					// we don't accidentally test something that doesn't follow the path
   380  					// we're intending to follow.
   381  					client.indexErr = errors.New("not using a cache")
   382  					So(errors.Unwrap(tc.fn()), ShouldEqual, client.indexErr)
   383  
   384  					for i := 0; i < 10; i++ {
   385  						if i == 0 {
   386  							// First time successfully reads the index.
   387  							client.indexErr = nil
   388  						} else {
   389  							// Subsequent attempts to load the index will result in an error.
   390  							// This ensures that if they are successful, it's because we're
   391  							// hitting the cache.
   392  							client.indexErr = errors.New("not using a cache")
   393  						}
   394  
   395  						So(tc.fn(), ShouldBeNil)
   396  					}
   397  				})
   398  			})
   399  		}
   400  
   401  		Convey(`Tail with no log entries returns ErrDoesNotExist.`, func() {
   402  			client.load(&gen)
   403  
   404  			_, err := st.Tail(c, "", "")
   405  			So(err, ShouldEqual, storage.ErrDoesNotExist)
   406  		})
   407  	})
   408  }
   409  
   410  func TestArchiveStorage(t *testing.T) {
   411  	t.Parallel()
   412  	testArchiveStorage(t, -1)
   413  }
   414  
   415  func TestArchiveStorageWithLimit(t *testing.T) {
   416  	t.Parallel()
   417  	testArchiveStorage(t, 4)
   418  }