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 }