@@ -2,8 +2,10 @@ package executor
22
33import (
44 "bytes"
5+ "fmt"
56 "io"
67 "os"
8+ "strings"
79 "testing"
810 "time"
911
@@ -145,3 +147,92 @@ func TestSplitOutputLines(t *testing.T) {
145147 })
146148 }
147149}
150+
151+ func TestLineCapture (t * testing.T ) {
152+ tbl := []struct {
153+ name string
154+ in string
155+ res []string
156+ }{
157+ {"empty" , "" , nil },
158+ {"single line, no trailing newline" , "hello" , []string {"hello" }},
159+ {"single line with trailing newline" , "hello\n " , []string {"hello" }},
160+ {"multiple lines" , "line1\n line2\n line3\n " , []string {"line1" , "line2" , "line3" }},
161+ {"blank line in the middle" , "line1\n \n line2\n " , []string {"line1" , "" , "line2" }},
162+ {"single newline" , "\n " , []string {"" }},
163+ {"trailing blank line" , "line1\n \n " , []string {"line1" , "" }},
164+ {"crlf line endings" , "line1\r \n line2\r \n " , []string {"line1" , "line2" }},
165+ }
166+
167+ // a nil predicate reproduces splitOutputLines whatever the write boundaries are, including a
168+ // chunk size that splits a line, a crlf pair or a run of newlines
169+ for _ , tt := range tbl {
170+ for _ , chunk := range []int {0 , 1 , 2 , 3 , 7 } {
171+ t .Run (fmt .Sprintf ("%s/chunk %d" , tt .name , chunk ), func (t * testing.T ) {
172+ lc := & lineCapture {}
173+ writeInChunks (t , lc , tt .in , chunk )
174+ assert .Equal (t , tt .res , lc .result ())
175+ assert .Equal (t , splitOutputLines (tt .in ), lc .result (), "result is idempotent and matches the batch split" )
176+ })
177+ }
178+ }
179+ }
180+
181+ func TestLineCaptureKeepLine (t * testing.T ) {
182+ t .Run ("keeps only accepted lines" , func (t * testing.T ) {
183+ lc := & lineCapture {keep : func (line string ) bool { return strings .HasPrefix (line , "setvar " ) }}
184+ writeInChunks (t , lc , "noise\n setvar a=1\n more noise\n setvar b=2\n " , 3 )
185+ assert .Equal (t , []string {"setvar a=1" , "setvar b=2" }, lc .result ())
186+ })
187+
188+ t .Run ("rejecting everything retains nothing" , func (t * testing.T ) {
189+ lc := & lineCapture {keep : func (string ) bool { return false }}
190+ writeInChunks (t , lc , "one\n two\n three" , 0 )
191+ assert .Nil (t , lc .result ())
192+ })
193+
194+ t .Run ("predicate sees an unterminated final line" , func (t * testing.T ) {
195+ var seen []string
196+ lc := & lineCapture {keep : func (line string ) bool { seen = append (seen , line ); return true }}
197+ writeInChunks (t , lc , "first\n last-no-newline" , 4 )
198+ assert .Equal (t , []string {"first" }, seen , "the final line is only offered once result is called" )
199+ assert .Equal (t , []string {"first" , "last-no-newline" }, lc .result ())
200+ })
201+
202+ t .Run ("line longer than a scanner token limit survives" , func (t * testing.T ) {
203+ long := strings .Repeat ("x" , 1 << 20 )
204+ lc := & lineCapture {}
205+ writeInChunks (t , lc , long + "\n short\n " , 4096 )
206+ require .Len (t , lc .result (), 2 )
207+ assert .Equal (t , long , lc .result ()[0 ])
208+ assert .Equal (t , "short" , lc .result ()[1 ])
209+ })
210+ }
211+
212+ // writeInChunks feeds s to w in fixed-size pieces, or in one write when size is 0, so a test can
213+ // pin behavior across the write boundaries a real command produces.
214+ func writeInChunks (t * testing.T , w io.Writer , s string , size int ) {
215+ t .Helper ()
216+ if size <= 0 {
217+ _ , err := w .Write ([]byte (s ))
218+ require .NoError (t , err )
219+ return
220+ }
221+ for i := 0 ; i < len (s ); i += size {
222+ end := min (i + size , len (s ))
223+ _ , err := w .Write ([]byte (s [i :end ]))
224+ require .NoError (t , err )
225+ }
226+ }
227+
228+ func TestLineCaptureReleasesLargeStagingBuffer (t * testing.T ) {
229+ // a long line arriving in pieces has to be staged, but once it is consumed the buffer must go:
230+ // keeping it would pin the longest line for as long as the command runs, which is the retention
231+ // this type exists to remove
232+ lc := & lineCapture {keep : func (string ) bool { return false }}
233+ writeInChunks (t , lc , strings .Repeat ("x" , 4 << 20 )+ "\n " , 4096 )
234+ assert .LessOrEqual (t , cap (lc .partial ), 64 << 10 , "the staging buffer is released once the line is consumed" )
235+
236+ writeInChunks (t , lc , "short\n " , 2 ) // still usable for the output that follows
237+ assert .Nil (t , lc .result ())
238+ }
0 commit comments