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 }