go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/bundler/builder_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 bundler
    16  
    17  import (
    18  	"fmt"
    19  	"strconv"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	. "github.com/smartystreets/goconvey/convey"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/common/clock/testclock"
    28  	"go.chromium.org/luci/logdog/api/logpb"
    29  )
    30  
    31  func parse(desc string) (*logpb.ButlerLogBundle_Entry, []*logpb.LogEntry) {
    32  	comp := strings.Split(desc, ":")
    33  	name, entries := comp[0], comp[1:]
    34  
    35  	be := &logpb.ButlerLogBundle_Entry{
    36  		Desc: &logpb.LogStreamDescriptor{
    37  			Name: name,
    38  		},
    39  	}
    40  
    41  	logs := make([]*logpb.LogEntry, len(entries))
    42  	for idx, l := range entries {
    43  		comp := strings.SplitN(l, "@", 2)
    44  		key, size := comp[0], 0
    45  		if len(comp) == 2 {
    46  			size, _ = strconv.Atoi(comp[1])
    47  		}
    48  
    49  		le := &logpb.LogEntry{
    50  			Content: &logpb.LogEntry_Text{Text: &logpb.Text{
    51  				Lines: []*logpb.Text_Line{
    52  					{Value: []byte(key)},
    53  				},
    54  			}},
    55  		}
    56  
    57  		// Pad missing data, if requested.
    58  		if size > 0 {
    59  			missing := size - protoSize(le)
    60  			if missing > 0 {
    61  				le.GetText().Lines = append(le.GetText().Lines, &logpb.Text_Line{
    62  					Value: []byte(strings.Repeat("!", missing)),
    63  				})
    64  			}
    65  		}
    66  		logs[idx] = le
    67  	}
    68  	return be, logs
    69  }
    70  
    71  func logEntryName(le *logpb.LogEntry) string {
    72  	t := le.GetText()
    73  	if t == nil || len(t.Lines) == 0 {
    74  		return ""
    75  	}
    76  	return string(t.Lines[0].Value)
    77  }
    78  
    79  // "expected" is a notation to express a bundle entry and its keys:
    80  //
    81  //	"a": a bundle entry keyed on "a".
    82  //	"+a": a terminal bundle entry keyed on "a".
    83  //	"a:1:2:3": a bundle entry keyed on "a" with three log entries, each keyed on
    84  //	         "1", "2", and "3" respectively.
    85  func shouldHaveBundleEntries(actual any, expected ...any) string {
    86  	bundle := actual.(*logpb.ButlerLogBundle)
    87  
    88  	var errors []string
    89  	fail := func(f string, args ...any) {
    90  		errors = append(errors, fmt.Sprintf(f, args...))
    91  	}
    92  
    93  	term := make(map[string]bool)
    94  	exp := make(map[string][]string)
    95  
    96  	// Parse expectation strings.
    97  	for _, e := range expected {
    98  		s := e.(string)
    99  		if len(s) == 0 {
   100  			continue
   101  		}
   102  
   103  		t := false
   104  		if s[0] == '+' {
   105  			t = true
   106  			s = s[1:]
   107  		}
   108  
   109  		parts := strings.Split(s, ":")
   110  		name := parts[0]
   111  		term[name] = t
   112  
   113  		if len(parts) > 1 {
   114  			exp[name] = append(exp[name], parts[1:]...)
   115  		}
   116  	}
   117  
   118  	entries := make(map[string]*logpb.ButlerLogBundle_Entry)
   119  	for _, be := range bundle.Entries {
   120  		entries[be.Desc.Name] = be
   121  	}
   122  	for name, t := range term {
   123  		be := entries[name]
   124  		if be == nil {
   125  			fail("No bundle entry for [%s]", name)
   126  			continue
   127  		}
   128  		delete(entries, name)
   129  
   130  		if t != be.Terminal {
   131  			fail("Bundle entry [%s] doesn't match expected terminal state (exp: %v != act: %v)",
   132  				name, t, be.Terminal)
   133  		}
   134  
   135  		logs := exp[name]
   136  		for i, l := range logs {
   137  			if i >= len(be.Logs) {
   138  				fail("Bundle entry [%s] missing log: %s", name, l)
   139  				continue
   140  			}
   141  			le := be.Logs[i]
   142  
   143  			if logEntryName(le) != l {
   144  				fail("Bundle entry [%s] log %d doesn't match expected (exp: %s != act: %s)",
   145  					name, i, l, logEntryName(le))
   146  				continue
   147  			}
   148  		}
   149  		if len(be.Logs) > len(logs) {
   150  			for _, le := range be.Logs[len(logs):] {
   151  				fail("Bundle entry [%s] has extra log entry: %s", name, logEntryName(le))
   152  			}
   153  		}
   154  	}
   155  	for k := range entries {
   156  		fail("Unexpected bundle entry present: [%s]", k)
   157  	}
   158  	return strings.Join(errors, "\n")
   159  }
   160  
   161  func TestBuilder(t *testing.T) {
   162  	Convey(`A builder`, t, func() {
   163  		tc := testclock.New(time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC))
   164  		b := &builder{
   165  			template: logpb.ButlerLogBundle{
   166  				Timestamp: timestamppb.New(tc.Now()),
   167  			},
   168  		}
   169  		templateSize := protoSize(&b.template)
   170  
   171  		Convey(`Is not ready by default, and has no content.`, func() {
   172  			b.size = templateSize + 1
   173  			So(b.ready(), ShouldBeFalse)
   174  			So(b.hasContent(), ShouldBeFalse)
   175  
   176  			Convey(`When exceeding the desired size with content, is ready.`, func() {
   177  				be, _ := parse("a")
   178  				b.size = 1
   179  				b.setStreamTerminal(be, 0)
   180  				So(b.ready(), ShouldBeTrue)
   181  			})
   182  		})
   183  
   184  		Convey(`Has a bundleSize() and remaining value of the template.`, func() {
   185  			b.size = 1024
   186  
   187  			So(b.bundleSize(), ShouldEqual, templateSize)
   188  			So(b.remaining(), ShouldEqual, 1024-templateSize)
   189  		})
   190  
   191  		Convey(`With a size of 1024 and a 512-byte LogEntry, has content, but is not ready.`, func() {
   192  			b.size = 1024
   193  			be, logs := parse("a:1@512")
   194  			b.add(be, logs[0])
   195  			So(b.hasContent(), ShouldBeTrue)
   196  			So(b.ready(), ShouldBeFalse)
   197  
   198  			Convey(`After adding another 512-byte LogEntry, is ready.`, func() {
   199  				be, logs := parse("a:2@512")
   200  				b.add(be, logs[0])
   201  				So(b.ready(), ShouldBeTrue)
   202  			})
   203  		})
   204  
   205  		Convey(`Has content after adding a terminal entry.`, func() {
   206  			So(b.hasContent(), ShouldBeFalse)
   207  			be, _ := parse("a")
   208  			b.setStreamTerminal(be, 1024)
   209  			So(b.hasContent(), ShouldBeTrue)
   210  		})
   211  
   212  		for _, test := range []struct {
   213  			title string
   214  
   215  			streams  []string
   216  			terminal bool
   217  			expected []string
   218  		}{
   219  			{`Empty terminal entry`,
   220  				[]string{"a"}, true, []string{"+a"}},
   221  			{`Single non-terminal entry`,
   222  				[]string{"a:1"}, false, []string{"a:1"}},
   223  			{`Multiple non-terminal entries`,
   224  				[]string{"a:1:2:3:4"}, false, []string{"a:1:2:3:4"}},
   225  			{`Single large entry`,
   226  				[]string{"a:1@1024"}, false, []string{"a:1"}},
   227  			{`Multiple terminal streams.`,
   228  				[]string{"a:1", "b:1", "a:2", "c:1"}, true, []string{"+a:1:2", "+b:1", "+c:1"}},
   229  			{`Multiple large non-terminal streams.`,
   230  				[]string{"a:1@1024", "b:1@8192", "a:2@4096", "c:1"}, false, []string{"a:1:2", "b:1", "c:1"}},
   231  		} {
   232  			Convey(fmt.Sprintf(`Test Case: %q`, test.title), func() {
   233  				for _, s := range test.streams {
   234  					be, logs := parse(s)
   235  					for _, le := range logs {
   236  						b.add(be, le)
   237  					}
   238  
   239  					if test.terminal {
   240  						b.setStreamTerminal(be, 1)
   241  					}
   242  				}
   243  
   244  				Convey(`Constructed bundle matches expected.`, func() {
   245  					islice := make([]any, len(test.expected))
   246  					for i, exp := range test.expected {
   247  						islice[i] = exp
   248  					}
   249  					So(b.bundle(), shouldHaveBundleEntries, islice...)
   250  				})
   251  
   252  				Convey(`Calculated size matches actual.`, func() {
   253  					So(b.bundleSize(), ShouldEqual, protoSize(b.bundle()))
   254  				})
   255  			})
   256  		}
   257  	})
   258  }