go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/appengine/coordinator/logStream_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 coordinator 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 "time" 22 23 . "github.com/smartystreets/goconvey/convey" 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/clock/testclock" 28 "go.chromium.org/luci/common/data/stringset" 29 . "go.chromium.org/luci/common/testing/assertions" 30 "go.chromium.org/luci/gae/impl/memory" 31 ds "go.chromium.org/luci/gae/service/datastore" 32 "go.chromium.org/luci/logdog/api/logpb" 33 "go.chromium.org/luci/logdog/common/types" 34 ) 35 36 func shouldHaveLogPaths(actual any, expected ...any) string { 37 names := stringset.New(len(expected)) 38 switch t := actual.(type) { 39 case error: 40 return t.Error() 41 42 case []*LogStream: 43 for _, ls := range t { 44 names.Add(string(ls.Path())) 45 } 46 47 default: 48 return fmt.Sprintf("unknown 'actual' type: %T", t) 49 } 50 51 exp := stringset.New(len(expected)) 52 for _, v := range expected { 53 s, ok := v.(string) 54 if !ok { 55 panic("non-string stream name specified") 56 } 57 exp.Add(s) 58 } 59 return ShouldBeEmpty(names.Difference(exp)) 60 } 61 62 func updateLogStreamID(ls *LogStream) { 63 ls.ID = LogStreamID(ls.Path()) 64 } 65 66 func TestLogStream(t *testing.T) { 67 t.Parallel() 68 69 Convey(`A testing log stream`, t, func() { 70 c, tc := testclock.UseTime(context.Background(), testclock.TestTimeLocal) 71 c = memory.Use(c) 72 ds.GetTestable(c).AutoIndex(true) 73 ds.GetTestable(c).Consistent(true) 74 75 now := ds.RoundTime(tc.Now().UTC()) 76 77 ls := LogStream{ 78 ID: LogStreamID("testing/+/log/stream"), 79 Prefix: "testing", 80 Name: "log/stream", 81 Created: now.UTC(), 82 ExpireAt: now.Add(LogStreamExpiry).UTC(), 83 } 84 85 desc := &logpb.LogStreamDescriptor{ 86 Prefix: "testing", 87 Name: "log/stream", 88 StreamType: logpb.StreamType_TEXT, 89 ContentType: string(types.ContentTypeText), 90 Timestamp: timestamppb.New(now), 91 Tags: map[string]string{ 92 "foo": "bar", 93 "baz": "qux", 94 "quux": "", 95 }, 96 } 97 98 Convey(`Can populate the LogStream with descriptor state.`, func() { 99 So(ls.LoadDescriptor(desc), ShouldBeNil) 100 So(ls.Validate(), ShouldBeNil) 101 102 Convey(`Will not validate`, func() { 103 Convey(`Without a valid Prefix`, func() { 104 ls.Prefix = "!!!not a valid prefix!!!" 105 updateLogStreamID(&ls) 106 107 So(ls.Validate(), ShouldErrLike, "invalid prefix") 108 }) 109 Convey(`Without a valid Name`, func() { 110 ls.Name = "!!!not a valid name!!!" 111 updateLogStreamID(&ls) 112 113 So(ls.Validate(), ShouldErrLike, "invalid name") 114 }) 115 Convey(`Without a valid created time`, func() { 116 ls.Created = time.Time{} 117 So(ls.Validate(), ShouldErrLike, "created time is not set") 118 }) 119 Convey(`With an invalid descriptor protobuf`, func() { 120 ls.Descriptor = []byte{0x00} // Invalid tag, "0". 121 So(ls.Validate(), ShouldErrLike, "could not unmarshal descriptor") 122 }) 123 }) 124 125 Convey(`Can write the LogStream to the Datastore.`, func() { 126 So(ds.Put(c, &ls), ShouldBeNil) 127 128 Convey(`Can read the LogStream back from the Datastore.`, func() { 129 ls2 := LogStream{ID: ls.ID} 130 So(ds.Get(c, &ls2), ShouldBeNil) 131 So(ls2, ShouldResemble, ls) 132 }) 133 }) 134 }) 135 136 Convey(`Will refuse to populate from an invalid descriptor.`, func() { 137 desc.StreamType = -1 138 So(ls.LoadDescriptor(desc), ShouldErrLike, "invalid descriptor") 139 }) 140 141 Convey(`Writing multiple LogStream entries`, func() { 142 times := map[string]*timestamppb.Timestamp{} 143 streamPaths := []string{ 144 "testing/+/foo/bar", 145 "testing/+/foo/bar/baz", 146 "testing/+/baz/qux", 147 "testing/+/cat/dog", 148 "testing/+/cat/bird/dog", 149 "testing/+/bird/plane", 150 } 151 for i, path := range streamPaths { 152 _, splitName := types.StreamPath(path).Split() 153 name := string(splitName) 154 155 lsCopy := ls 156 lsCopy.Name = name 157 lsCopy.Created = ds.RoundTime(now.Add(time.Duration(i) * time.Second)) 158 lsCopy.ExpireAt = lsCopy.Created.Add(LogStreamExpiry) 159 updateLogStreamID(&lsCopy) 160 161 descCopy := proto.Clone(desc).(*logpb.LogStreamDescriptor) 162 descCopy.Name = name 163 164 if err := lsCopy.LoadDescriptor(descCopy); err != nil { 165 panic(fmt.Errorf("in %#v: %s", descCopy, err)) 166 } 167 So(ds.Put(c, &lsCopy), ShouldBeNil) 168 169 times[name] = timestamppb.New(lsCopy.Created) 170 } 171 172 getAll := func(q *LogStreamQuery) []*LogStream { 173 var streams []*LogStream 174 err := q.Run(c, func(ls *LogStream, _ ds.CursorCB) error { 175 streams = append(streams, ls) 176 return nil 177 }) 178 So(err, ShouldBeNil) 179 return streams 180 } 181 182 Convey(`When querying LogStream`, func() { 183 Convey(`LogStream path queries`, func() { 184 Convey(`A query for "foo/bar" should return "foo/bar".`, func() { 185 q, err := NewLogStreamQuery("testing/+/foo/bar") 186 So(err, ShouldBeNil) 187 188 So(getAll(q), shouldHaveLogPaths, "testing/+/foo/bar") 189 }) 190 191 Convey(`A query for "foo/bar/*" should return "foo/bar/baz".`, func() { 192 q, err := NewLogStreamQuery("testing/+/foo/bar/*") 193 So(err, ShouldBeNil) 194 195 So(getAll(q), shouldHaveLogPaths, "testing/+/foo/bar/baz") 196 }) 197 198 Convey(`A query for "foo/**" should return "foo/bar/baz" and "foo/bar".`, func() { 199 q, err := NewLogStreamQuery("testing/+/foo/**") 200 So(err, ShouldBeNil) 201 202 So(getAll(q), shouldHaveLogPaths, 203 "testing/+/foo/bar/baz", "testing/+/foo/bar") 204 }) 205 206 Convey(`A query for "cat/**/dog" should return "cat/dog" and "cat/bird/dog".`, func() { 207 q, err := NewLogStreamQuery("testing/+/cat/**/dog") 208 So(err, ShouldBeNil) 209 210 So(getAll(q), shouldHaveLogPaths, 211 "testing/+/cat/bird/dog", 212 "testing/+/cat/dog", 213 ) 214 }) 215 }) 216 217 Convey(`A timestamp inequality query for all records returns them in reverse order.`, func() { 218 // Reverse "streamPaths". 219 si := make([]any, len(streamPaths)) 220 for i := 0; i < len(streamPaths); i++ { 221 si[i] = any(streamPaths[len(streamPaths)-i-1]) 222 } 223 224 q, err := NewLogStreamQuery("testing") 225 So(err, ShouldBeNil) 226 So(getAll(q), shouldHaveLogPaths, si...) 227 }) 228 229 Convey(`A query for "cat/**/dog" should return "cat/bird/dog" and "cat/dog".`, func() { 230 q, err := NewLogStreamQuery("testing/+/cat/**/dog") 231 So(err, ShouldBeNil) 232 233 So(getAll(q), shouldHaveLogPaths, 234 "testing/+/cat/bird/dog", "testing/+/cat/dog") 235 }) 236 237 }) 238 }) 239 }) 240 } 241 242 func TestNewLogStreamGlob(t *testing.T) { 243 t.Parallel() 244 245 mkLS := func(path string, now time.Time) *LogStream { 246 prefix, name := types.StreamPath(path).Split() 247 ret := &LogStream{Created: now, ExpireAt: now.Add(LogStreamExpiry)} 248 So(ret.LoadDescriptor(&logpb.LogStreamDescriptor{ 249 Prefix: string(prefix), 250 Name: string(name), 251 ContentType: string(types.ContentTypeText), 252 Timestamp: timestamppb.New(now), 253 }), ShouldBeNil) 254 updateLogStreamID(ret) 255 return ret 256 } 257 258 getAllMatches := func(q *LogStreamQuery, logPaths ...string) []*LogStream { 259 ctx := memory.Use(context.Background()) 260 ds.GetTestable(ctx).AutoIndex(true) 261 ds.GetTestable(ctx).Consistent(true) 262 263 logStreams := make([]*LogStream, len(logPaths)) 264 now := testclock.TestTimeUTC 265 for i, path := range logPaths { 266 logStreams[i] = mkLS(path, now) 267 now = now.Add(time.Second) 268 } 269 So(ds.Put(ctx, logStreams), ShouldBeNil) 270 271 var streams []*LogStream 272 err := q.Run(ctx, func(ls *LogStream, _ ds.CursorCB) error { 273 streams = append(streams, ls) 274 return nil 275 }) 276 So(err, ShouldBeNil) 277 return streams 278 } 279 280 Convey(`A testing query`, t, func() { 281 Convey(`Will construct a non-globbing query as Prefix/Name equality.`, func() { 282 q, err := NewLogStreamQuery("foo/bar/+/baz/qux") 283 So(err, ShouldBeNil) 284 285 So(getAllMatches(q, 286 "foo/bar/+/baz/qux", 287 288 "foo/bar/+/baz/qux/other", 289 "foo/bar/+/baz", 290 "other/prefix/+/baz/qux", 291 ), shouldHaveLogPaths, 292 "foo/bar/+/baz/qux", 293 ) 294 }) 295 296 Convey(`Will refuse to query an invalid Prefix/Name.`, func() { 297 _, err := NewLogStreamQuery("////+/baz/qux") 298 So(err, ShouldErrLike, "prefix invalid") 299 300 _, err = NewLogStreamQuery("foo/bar/+//////") 301 So(err, ShouldErrLike, "name invalid") 302 }) 303 304 Convey(`Returns error on empty prefix.`, func() { 305 _, err := NewLogStreamQuery("/+/baz/qux") 306 So(err, ShouldErrLike, "prefix invalid: empty") 307 }) 308 309 Convey(`Treats empty name like **.`, func() { 310 q, err := NewLogStreamQuery("baz/qux") 311 So(err, ShouldBeNil) 312 313 So(getAllMatches(q, 314 "baz/qux/+/narp", 315 "baz/qux/+/blats/stuff", 316 "baz/qux/+/nerds/cool_pants", 317 318 "other/prefix/+/baz/qux", 319 ), shouldHaveLogPaths, 320 "baz/qux/+/nerds/cool_pants", 321 "baz/qux/+/blats/stuff", 322 "baz/qux/+/narp", 323 ) 324 }) 325 326 Convey(`Properly escapes non-* metachars.`, func() { 327 q, err := NewLogStreamQuery("baz/qux/+/hi..../**") 328 So(err, ShouldBeNil) 329 330 So(getAllMatches(q, 331 "baz/qux/+/hi....", 332 "baz/qux/+/hi..../some_stuff", 333 334 "baz/qux/+/hiblat", 335 "baz/qux/+/hiblat/some_stuff", 336 ), shouldHaveLogPaths, 337 "baz/qux/+/hi..../some_stuff", 338 "baz/qux/+/hi....", 339 ) 340 }) 341 342 Convey(`Will glob out single Name components.`, func() { 343 q, err := NewLogStreamQuery("pfx/+/foo/*/*/bar/*/baz/qux/*") 344 So(err, ShouldBeNil) 345 346 So(getAllMatches(q, 347 "pfx/+/foo/a/b/bar/c/baz/qux/d", 348 349 "pfx/+/foo/bar/baz/qux", 350 "pfx/+/foo/a/extra/b/bar/c/baz/qux/d", 351 ), shouldHaveLogPaths, 352 "pfx/+/foo/a/b/bar/c/baz/qux/d", 353 ) 354 }) 355 356 Convey(`Will handle end-of-query globbing.`, func() { 357 q, err := NewLogStreamQuery("pfx/+/foo/*/bar/**") 358 So(err, ShouldBeNil) 359 360 So(getAllMatches(q, 361 "pfx/+/foo/a/bar", 362 "pfx/+/foo/a/bar/stuff", 363 "pfx/+/foo/a/bar/even/more/stuff", 364 365 "pfx/+/foo/a/extra/bar", 366 "pfx/+/nope/a/bar", 367 ), shouldHaveLogPaths, 368 "pfx/+/foo/a/bar/even/more/stuff", 369 "pfx/+/foo/a/bar/stuff", 370 "pfx/+/foo/a/bar", 371 ) 372 }) 373 374 Convey(`Will handle beginning-of-query globbing.`, func() { 375 q, err := NewLogStreamQuery("pfx/+/**/foo/*/bar") 376 So(err, ShouldBeNil) 377 378 So(getAllMatches(q, 379 "pfx/+/extra/foo/a/bar", 380 "pfx/+/even/more/extra/foo/a/bar", 381 "pfx/+/foo/a/bar", 382 383 "pfx/+/foo/a/bar/extra", 384 "pfx/+/foo/bar", 385 ), shouldHaveLogPaths, 386 "pfx/+/foo/a/bar", 387 "pfx/+/even/more/extra/foo/a/bar", 388 "pfx/+/extra/foo/a/bar", 389 ) 390 }) 391 392 Convey(`Can handle middle-of-query globbing.`, func() { 393 q, err := NewLogStreamQuery("pfx/+/*/foo/*/**/bar/*/baz/*") 394 So(err, ShouldBeNil) 395 396 So(getAllMatches(q, 397 "pfx/+/a/foo/b/stuff/bar/c/baz/d", 398 "pfx/+/a/foo/b/lots/of/stuff/bar/c/baz/d", 399 "pfx/+/a/foo/b/bar/c/baz/d", 400 401 "pfx/+/foo/a/bar/b/baz/c", 402 ), shouldHaveLogPaths, 403 "pfx/+/a/foo/b/bar/c/baz/d", 404 "pfx/+/a/foo/b/lots/of/stuff/bar/c/baz/d", 405 "pfx/+/a/foo/b/stuff/bar/c/baz/d", 406 ) 407 }) 408 }) 409 }