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  }