blob: db3903a7accc916d9d8b6cf3214099b79167f582 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bundler
import (
"fmt"
"strings"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/luci/logdog/api/logpb"
)
type textTestCase struct {
title string
delimiter string
source []string
limit int
increment time.Duration
allowSplit bool
closed bool
out []textTestOutput
}
type textTestOutput struct {
seq int64
lines []string
increment time.Duration
}
func (o *textTestOutput) testLines() logpb.Text {
t := logpb.Text{}
for _, line := range o.lines {
delim := ""
switch {
case strings.HasSuffix(line, windowsNewline):
delim = windowsNewline
case strings.HasSuffix(line, posixNewline):
delim = posixNewline
}
val := []byte(line[:len(line)-len(delim)])
if len(val) == 0 {
val = nil
}
t.Lines = append(t.Lines, &logpb.Text_Line{
Value: val,
Delimiter: delim,
})
}
return t
}
func TestTextParser(t *testing.T) {
Convey(`Using a parser test stream`, t, func() {
s := &parserTestStream{
now: time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC),
prefixIndex: 1337,
}
for _, tst := range []textTestCase{
{
title: `Standard parsing with funky newline.`,
source: []string{"foo\nbar\r\nbaz\n"},
out: []textTestOutput{
{lines: []string{"foo\n", "bar\r\n", "baz\n"}},
},
},
{
title: `Will not parse an undelimited line when not truncating.`,
source: []string{"foo\nbar\nbaz"},
out: []textTestOutput{
{lines: []string{"foo\n", "bar\n"}},
},
},
{
title: `Will not parse dangling \r when truncating but not closed.`,
source: []string{"foo\r\nbar\r\nbaz\r"},
allowSplit: true,
out: []textTestOutput{
{lines: []string{"foo\r\n", "bar\r\n", "baz"}},
},
},
{
title: `Will parse dangling \r when truncating and closed.`,
source: []string{"foo\r\nbar\r\nbaz\r"},
allowSplit: true,
closed: true,
out: []textTestOutput{
{lines: []string{"foo\r\n", "bar\r\n", "baz\r"}},
},
},
{
title: `Will not increase sequence for partial lines.`,
source: []string{"foobar\rbaz\nq\nux"},
limit: 3,
allowSplit: true,
out: []textTestOutput{
{lines: []string{"foo"}},
{lines: []string{"bar"}},
{lines: []string{"\rba"}},
{lines: []string{"z\n", "q"}},
{seq: 1, lines: []string{"\n", "ux"}},
},
},
{
title: `Will obey the limit if it yields a line but truncates the next.`,
source: []string{"foo\nbar\nbaz"},
limit: 5,
out: []textTestOutput{
{lines: []string{"foo\n"}},
{seq: 1, lines: []string{"bar\n"}},
},
},
{
title: `Can parse unicode strings.`,
source: []string{"TEST©\r\n©\r\n©"},
allowSplit: true,
closed: true,
out: []textTestOutput{
{lines: []string{"TEST©\r\n", "©\r\n", "©"}},
},
},
{
title: `Unicode string with a split two-byte character should split across boundary.`,
source: []string{"hA©\n"},
allowSplit: true,
limit: 3,
out: []textTestOutput{
{lines: []string{"hA"}},
{lines: []string{"©\n"}},
},
},
{
title: `A two-byte Unicode glyph will return nothing with a limit of 1.`,
source: []string{"©\n"},
limit: 1,
allowSplit: true,
out: []textTestOutput{},
},
{
title: `Multiple chunks with different timestamps across newline boundaries`,
source: []string{"fo", "o\nb", "ar\n", "baz\nqux\n"},
increment: time.Second,
out: []textTestOutput{
{seq: 0, lines: []string{"foo\n"}},
{seq: 1, lines: []string{"bar\n"}, increment: time.Second},
{seq: 2, lines: []string{"baz\n", "qux\n"}, increment: 2 * time.Second},
},
},
{
title: `Will parse end of line when closed.`,
source: []string{"foo\nbar\nbaz"},
allowSplit: true,
closed: true,
out: []textTestOutput{
{lines: []string{"foo\n", "bar\n", "baz"}},
},
},
{
title: `Will parse empty lines from sequential mixed-OS delimiters.`,
source: []string{"\n\r\n\n\r\n\n\r\n\n\n\r\n\n\n"},
limit: 8,
out: []textTestOutput{
{lines: []string{"\n", "\r\n", "\n", "\r\n", "\n"}},
{seq: 5, lines: []string{"\r\n", "\n", "\n", "\r\n", "\n", "\n"}},
},
},
} {
if tst.limit == 0 {
tst.limit = 1024
}
Convey(fmt.Sprintf(`Test case: %q`, tst.title), func() {
p := &textParser{
baseParser: s.base(),
}
c := &constraints{
limit: tst.limit,
}
now := s.now
aggregate := []byte{}
for _, chunk := range tst.source {
p.Append(dstr(now, chunk))
aggregate = append(aggregate, []byte(chunk)...)
now = now.Add(tst.increment)
}
c.allowSplit = tst.allowSplit
c.closed = tst.closed
Convey(fmt.Sprintf(`Processes source %q.`, aggregate), func() {
for _, o := range tst.out {
le, err := p.nextEntry(c)
So(err, ShouldBeNil)
So(le, shouldMatchLogEntry, s.add(o.increment).le(o.seq, o.testLines()))
}
le, err := p.nextEntry(c)
So(err, ShouldBeNil)
So(le, ShouldBeNil)
})
})
}
})
}