go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/bundler/textParser_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  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	. "github.com/smartystreets/goconvey/convey"
    24  	"go.chromium.org/luci/logdog/api/logpb"
    25  )
    26  
    27  type textTestCase struct {
    28  	title      string
    29  	source     []string
    30  	limit      int
    31  	increment  time.Duration
    32  	allowSplit bool
    33  	closed     bool
    34  	out        []textTestOutput
    35  }
    36  
    37  type textTestOutput struct {
    38  	seq       int64
    39  	lines     []string
    40  	increment time.Duration
    41  }
    42  
    43  func (o *textTestOutput) testLines() logpb.Text {
    44  	t := logpb.Text{}
    45  	for _, line := range o.lines {
    46  		delim := ""
    47  		switch {
    48  		case strings.HasSuffix(line, windowsNewline):
    49  			delim = windowsNewline
    50  		case strings.HasSuffix(line, posixNewline):
    51  			delim = posixNewline
    52  		}
    53  
    54  		val := []byte(line[:len(line)-len(delim)])
    55  		if len(val) == 0 {
    56  			val = nil
    57  		}
    58  		t.Lines = append(t.Lines, &logpb.Text_Line{
    59  			Value:     val,
    60  			Delimiter: delim,
    61  		})
    62  	}
    63  	return t
    64  }
    65  
    66  func TestTextParser(t *testing.T) {
    67  	Convey(`Using a parser test stream`, t, func() {
    68  		s := &parserTestStream{
    69  			now:         time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC),
    70  			prefixIndex: 1337,
    71  		}
    72  
    73  		for _, tst := range []textTestCase{
    74  			{
    75  				title:  `Standard parsing with funky newline.`,
    76  				source: []string{"foo\nbar\r\nbaz\n"},
    77  				out: []textTestOutput{
    78  					{lines: []string{"foo\n", "bar\r\n", "baz\n"}},
    79  				},
    80  			},
    81  
    82  			{
    83  				title:  `Will not parse an undelimited line when not truncating.`,
    84  				source: []string{"foo\nbar\nbaz"},
    85  				out: []textTestOutput{
    86  					{lines: []string{"foo\n", "bar\n"}},
    87  				},
    88  			},
    89  
    90  			{
    91  				title:      `Will not parse dangling \r when truncating but not closed.`,
    92  				source:     []string{"foo\r\nbar\r\nbaz\r"},
    93  				allowSplit: true,
    94  				out: []textTestOutput{
    95  					{lines: []string{"foo\r\n", "bar\r\n", "baz"}},
    96  				},
    97  			},
    98  
    99  			{
   100  				title:      `Will parse dangling \r when truncating and closed.`,
   101  				source:     []string{"foo\r\nbar\r\nbaz\r"},
   102  				allowSplit: true,
   103  				closed:     true,
   104  				out: []textTestOutput{
   105  					{lines: []string{"foo\r\n", "bar\r\n", "baz\r"}},
   106  				},
   107  			},
   108  
   109  			{
   110  				title:      `Will not increase sequence for partial lines.`,
   111  				source:     []string{"foobar\rbaz\nq\nux"},
   112  				limit:      3,
   113  				allowSplit: true,
   114  				out: []textTestOutput{
   115  					{lines: []string{"foo"}},
   116  					{lines: []string{"bar"}},
   117  					{lines: []string{"\rba"}},
   118  					{lines: []string{"z\n", "q"}},
   119  					{seq: 1, lines: []string{"\n", "ux"}},
   120  				},
   121  			},
   122  
   123  			{
   124  				title:  `Will obey the limit if it yields a line but truncates the next.`,
   125  				source: []string{"foo\nbar\nbaz"},
   126  				limit:  5,
   127  				out: []textTestOutput{
   128  					{lines: []string{"foo\n"}},
   129  					{seq: 1, lines: []string{"bar\n"}},
   130  				},
   131  			},
   132  
   133  			{
   134  				title:      `Can parse unicode strings.`,
   135  				source:     []string{"TEST©\r\n©\r\n©"},
   136  				allowSplit: true,
   137  				closed:     true,
   138  				out: []textTestOutput{
   139  					{lines: []string{"TEST©\r\n", "©\r\n", "©"}},
   140  				},
   141  			},
   142  
   143  			{
   144  				title:      `Unicode string with a split two-byte character should split across boundary.`,
   145  				source:     []string{"hA©\n"},
   146  				allowSplit: true,
   147  				limit:      3,
   148  				out: []textTestOutput{
   149  					{lines: []string{"hA"}},
   150  					{lines: []string{"©\n"}},
   151  				},
   152  			},
   153  
   154  			{
   155  				title:      `A two-byte Unicode glyph will return nothing with a limit of 1.`,
   156  				source:     []string{"©\n"},
   157  				limit:      1,
   158  				allowSplit: true,
   159  				out:        []textTestOutput{},
   160  			},
   161  
   162  			{
   163  				title:     `Multiple chunks with different timestamps across newline boundaries`,
   164  				source:    []string{"fo", "o\nb", "ar\n", "baz\nqux\n"},
   165  				increment: time.Second,
   166  				out: []textTestOutput{
   167  					{seq: 0, lines: []string{"foo\n"}},
   168  					{seq: 1, lines: []string{"bar\n"}, increment: time.Second},
   169  					{seq: 2, lines: []string{"baz\n", "qux\n"}, increment: 2 * time.Second},
   170  				},
   171  			},
   172  			{
   173  				title:      `Will parse end of line when closed.`,
   174  				source:     []string{"foo\nbar\nbaz"},
   175  				allowSplit: true,
   176  				closed:     true,
   177  				out: []textTestOutput{
   178  					{lines: []string{"foo\n", "bar\n", "baz"}},
   179  				},
   180  			},
   181  
   182  			{
   183  				title:  `Will parse empty lines from sequential mixed-OS delimiters.`,
   184  				source: []string{"\n\r\n\n\r\n\n\r\n\n\n\r\n\n\n"},
   185  				limit:  8,
   186  				out: []textTestOutput{
   187  					{lines: []string{"\n", "\r\n", "\n", "\r\n", "\n"}},
   188  					{seq: 5, lines: []string{"\r\n", "\n", "\n", "\r\n", "\n", "\n"}},
   189  				},
   190  			},
   191  		} {
   192  			if tst.limit == 0 {
   193  				tst.limit = 1024
   194  			}
   195  
   196  			Convey(fmt.Sprintf(`Test case: %q`, tst.title), func() {
   197  				p := &textParser{
   198  					baseParser: s.base(),
   199  				}
   200  				c := &constraints{
   201  					limit: tst.limit,
   202  				}
   203  
   204  				now := s.now
   205  				aggregate := []byte{}
   206  				for _, chunk := range tst.source {
   207  					p.Append(dstr(now, chunk))
   208  					aggregate = append(aggregate, []byte(chunk)...)
   209  					now = now.Add(tst.increment)
   210  				}
   211  
   212  				c.allowSplit = tst.allowSplit
   213  				c.closed = tst.closed
   214  
   215  				Convey(fmt.Sprintf(`Processes source %q.`, aggregate), func() {
   216  					for _, o := range tst.out {
   217  						le, err := p.nextEntry(c)
   218  						So(err, ShouldBeNil)
   219  
   220  						So(le, shouldMatchLogEntry, s.add(o.increment).le(o.seq, o.testLines()))
   221  					}
   222  
   223  					le, err := p.nextEntry(c)
   224  					So(err, ShouldBeNil)
   225  					So(le, ShouldBeNil)
   226  				})
   227  			})
   228  		}
   229  	})
   230  }