| Index: client/internal/logdog/butler/bundler/builder_test.go
|
| diff --git a/client/internal/logdog/butler/bundler/builder_test.go b/client/internal/logdog/butler/bundler/builder_test.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..95b11facfd0b1d3a8ce548cd0322e6c3ec553ddc
|
| --- /dev/null
|
| +++ b/client/internal/logdog/butler/bundler/builder_test.go
|
| @@ -0,0 +1,265 @@
|
| +// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +package bundler
|
| +
|
| +import (
|
| + "fmt"
|
| + "strconv"
|
| + "strings"
|
| + "testing"
|
| + "time"
|
| +
|
| + "github.com/luci/luci-go/common/clock/testclock"
|
| + "github.com/luci/luci-go/common/logdog/protocol"
|
| + "github.com/luci/luci-go/common/proto/google"
|
| + . "github.com/smartystreets/goconvey/convey"
|
| +)
|
| +
|
| +func parse(desc string) (*protocol.ButlerLogBundle_Entry, []*protocol.LogEntry) {
|
| + comp := strings.Split(desc, ":")
|
| + name, entries := comp[0], comp[1:]
|
| +
|
| + be := &protocol.ButlerLogBundle_Entry{
|
| + Desc: &protocol.LogStreamDescriptor{
|
| + Name: name,
|
| + },
|
| + }
|
| +
|
| + logs := make([]*protocol.LogEntry, len(entries))
|
| + for idx, l := range entries {
|
| + comp := strings.SplitN(l, "@", 2)
|
| + key, size := comp[0], 0
|
| + if len(comp) == 2 {
|
| + size, _ = strconv.Atoi(comp[1])
|
| + }
|
| +
|
| + le := &protocol.LogEntry{
|
| + Content: &protocol.LogEntry_Text{Text: &protocol.Text{
|
| + Lines: []*protocol.Text_Line{
|
| + {Value: key},
|
| + },
|
| + }},
|
| + }
|
| +
|
| + // Pad missing data, if requested.
|
| + if size > 0 {
|
| + missing := size - protoSize(le)
|
| + if missing > 0 {
|
| + le.GetText().Lines = append(le.GetText().Lines, &protocol.Text_Line{
|
| + Value: strings.Repeat("!", missing),
|
| + })
|
| + }
|
| + }
|
| + logs[idx] = le
|
| + }
|
| + return be, logs
|
| +}
|
| +
|
| +// gen generates a ButlerLogBundle_Entry based on a description string.
|
| +//
|
| +// The string goes: "a:1:2:3", where "a" is the name of the stream and
|
| +// "1", "2", and "3", are different LogEntry within the stream.
|
| +//
|
| +// Note that the generated values are not valid, as they will be missing
|
| +// several fields. This is for bundling tests only :)
|
| +//
|
| +// Optionally, the string can include a size, e.g., "a:1@1024:...". This will
|
| +// cause additional data to be generated to pad the LogEntry out to the desired
|
| +// size. This is currently an approximation, as it doesn't take into account
|
| +// tag/array size overhead of the additional data.
|
| +func gen(desc string) *protocol.ButlerLogBundle_Entry {
|
| + be, logs := parse(desc)
|
| + be.Logs = logs
|
| + return be
|
| +}
|
| +
|
| +func logEntryName(le *protocol.LogEntry) string {
|
| + t := le.GetText()
|
| + if t == nil || len(t.Lines) == 0 {
|
| + return ""
|
| + }
|
| + return t.Lines[0].Value
|
| +}
|
| +
|
| +// "expected" is a notation to express a bundle entry and its keys:
|
| +// "a": a bundle entry keyed on "a".
|
| +// "+a": a terminal bundle entry keyed on "a".
|
| +// "a:1:2:3": a bundle entry keyed on "a" with three log entries, each keyed on
|
| +// "1", "2", and "3" respectively.
|
| +func shouldHaveBundleEntries(actual interface{}, expected ...interface{}) string {
|
| + bundle := actual.(*protocol.ButlerLogBundle)
|
| +
|
| + errors := []string{}
|
| + fail := func(f string, args ...interface{}) {
|
| + errors = append(errors, fmt.Sprintf(f, args...))
|
| + }
|
| +
|
| + term := make(map[string]bool)
|
| + exp := make(map[string][]string)
|
| +
|
| + // Parse expectation strings.
|
| + for _, e := range expected {
|
| + s := e.(string)
|
| + if len(s) == 0 {
|
| + continue
|
| + }
|
| +
|
| + t := false
|
| + if s[0] == '+' {
|
| + t = true
|
| + s = s[1:]
|
| + }
|
| +
|
| + parts := strings.Split(s, ":")
|
| + name := parts[0]
|
| + term[name] = t
|
| +
|
| + if len(parts) > 1 {
|
| + exp[name] = append(exp[name], parts[1:]...)
|
| + }
|
| + }
|
| +
|
| + entries := make(map[string]*protocol.ButlerLogBundle_Entry)
|
| + for _, be := range bundle.Entries {
|
| + entries[be.Desc.Name] = be
|
| + }
|
| + for name, t := range term {
|
| + be := entries[name]
|
| + if be == nil {
|
| + fail("No bundle entry for [%s]", name)
|
| + continue
|
| + }
|
| + delete(entries, name)
|
| +
|
| + if t != be.Terminal {
|
| + fail("Bundle entry [%s] doesn't match expected terminal state (exp: %v != act: %v)",
|
| + name, t, be.Terminal)
|
| + }
|
| +
|
| + logs := exp[name]
|
| + for i, l := range logs {
|
| + if i >= len(be.Logs) {
|
| + fail("Bundle entry [%s] missing log: %s", name, l)
|
| + continue
|
| + }
|
| + le := be.Logs[i]
|
| +
|
| + if logEntryName(le) != l {
|
| + fail("Bundle entry [%s] log %d doesn't match expected (exp: %s != act: %s)",
|
| + name, i, l, logEntryName(le))
|
| + continue
|
| + }
|
| + }
|
| + if len(be.Logs) > len(logs) {
|
| + for _, le := range be.Logs[len(logs):] {
|
| + fail("Bundle entry [%s] has extra log entry: %s", name, logEntryName(le))
|
| + }
|
| + }
|
| + }
|
| + for k := range entries {
|
| + fail("Unexpected bundle entry present: [%s]", k)
|
| + }
|
| + return strings.Join(errors, "\n")
|
| +}
|
| +
|
| +func TestBuilder(t *testing.T) {
|
| + Convey(`A builder`, t, func() {
|
| + tc := testclock.New(time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC))
|
| + b := &builder{
|
| + template: protocol.ButlerLogBundle{
|
| + Source: "Test Source",
|
| + Timestamp: google.NewTimestamp(tc.Now()),
|
| + },
|
| + }
|
| + templateSize := protoSize(&b.template)
|
| +
|
| + Convey(`Is not ready by default, and has no content.`, func() {
|
| + b.size = templateSize + 1
|
| + So(b.ready(), ShouldBeFalse)
|
| + So(b.hasContent(), ShouldBeFalse)
|
| +
|
| + Convey(`When exceeding the desired size with content, is ready.`, func() {
|
| + be, _ := parse("a")
|
| + b.size = 1
|
| + b.setStreamTerminal(be, 0)
|
| + So(b.ready(), ShouldBeTrue)
|
| + })
|
| + })
|
| +
|
| + Convey(`Has a bundleSize() and remaining value of the template.`, func() {
|
| + b.size = 1024
|
| +
|
| + So(b.bundleSize(), ShouldEqual, templateSize)
|
| + So(b.remaining(), ShouldEqual, 1024-templateSize)
|
| + })
|
| +
|
| + Convey(`With a size of 1024 and a 512-byte LogEntry, has content, but is not ready.`, func() {
|
| + b.size = 1024
|
| + be, logs := parse("a:1@512")
|
| + b.add(be, logs[0])
|
| + So(b.hasContent(), ShouldBeTrue)
|
| + So(b.ready(), ShouldBeFalse)
|
| +
|
| + Convey(`After adding another 512-byte LogEntry, is ready.`, func() {
|
| + be, logs := parse("a:2@512")
|
| + b.add(be, logs[0])
|
| + So(b.ready(), ShouldBeTrue)
|
| + })
|
| + })
|
| +
|
| + Convey(`Has content after adding a terminal entry.`, func() {
|
| + So(b.hasContent(), ShouldBeFalse)
|
| + be, _ := parse("a")
|
| + b.setStreamTerminal(be, 1024)
|
| + So(b.hasContent(), ShouldBeTrue)
|
| + })
|
| +
|
| + for _, test := range []struct {
|
| + title string
|
| +
|
| + streams []string
|
| + terminal bool
|
| + expected []string
|
| + }{
|
| + {`Empty terminal entry`,
|
| + []string{"a"}, true, []string{"+a"}},
|
| + {`Single non-terminal entry`,
|
| + []string{"a:1"}, false, []string{"a:1"}},
|
| + {`Multiple non-terminal entries`,
|
| + []string{"a:1:2:3:4"}, false, []string{"a:1:2:3:4"}},
|
| + {`Single large entry`,
|
| + []string{"a:1@1024"}, false, []string{"a:1"}},
|
| + {`Multiple terminal streams.`,
|
| + []string{"a:1", "b:1", "a:2", "c:1"}, true, []string{"+a:1:2", "+b:1", "+c:1"}},
|
| + {`Multiple large non-terminal streams.`,
|
| + []string{"a:1@1024", "b:1@8192", "a:2@4096", "c:1"}, false, []string{"a:1:2", "b:1", "c:1"}},
|
| + } {
|
| + Convey(fmt.Sprintf(`Test Case: %q`, test.title), func() {
|
| + for _, s := range test.streams {
|
| + be, logs := parse(s)
|
| + for _, le := range logs {
|
| + b.add(be, le)
|
| + }
|
| +
|
| + if test.terminal {
|
| + b.setStreamTerminal(be, 1)
|
| + }
|
| + }
|
| +
|
| + Convey(`Constructed bundle matches expected.`, func() {
|
| + islice := make([]interface{}, len(test.expected))
|
| + for i, exp := range test.expected {
|
| + islice[i] = exp
|
| + }
|
| + So(b.bundle(), shouldHaveBundleEntries, islice...)
|
| + })
|
| +
|
| + Convey(`Calculated size matches actual.`, func() {
|
| + So(b.bundleSize(), ShouldEqual, protoSize(b.bundle()))
|
| + })
|
| + })
|
| + }
|
| + })
|
| +}
|
|
|