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 }