1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
15
gc "launchpad.net/gocheck"
17
"launchpad.net/juju-core/testing"
18
"launchpad.net/juju-core/utils/tailer"
21
func Test(t *stdtesting.T) {
25
type tailerSuite struct{}
27
var _ = gc.Suite(tailerSuite{})
29
var alphabetData = []string{
35
"foxtrott foxtrott\n",
43
"november november\n",
58
var tests = []struct {
61
initialLinesWritten int
62
initialLinesRequested int
64
filter tailer.TailerFilterFunc
65
injector func(*tailer.Tailer, *readSeeker) func([]string)
66
initialCollectedData []string
67
appendedCollectedData []string
70
description: "lines are longer than buffer size",
72
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
73
"0123456789012345678901234567890123456789012345678901\n",
75
initialLinesWritten: 1,
76
initialLinesRequested: 1,
78
initialCollectedData: []string{
79
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
81
appendedCollectedData: []string{
82
"0123456789012345678901234567890123456789012345678901\n",
85
description: "lines are longer than buffer size, missing termination of last line",
87
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
88
"0123456789012345678901234567890123456789012345678901\n",
89
"the quick brown fox ",
91
initialLinesWritten: 1,
92
initialLinesRequested: 1,
94
initialCollectedData: []string{
95
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
97
appendedCollectedData: []string{
98
"0123456789012345678901234567890123456789012345678901\n",
101
description: "lines are longer than buffer size, last line is terminated later",
103
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
104
"0123456789012345678901234567890123456789012345678901\n",
105
"the quick brown fox ",
106
"jumps over the lazy dog\n",
108
initialLinesWritten: 1,
109
initialLinesRequested: 1,
111
initialCollectedData: []string{
112
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
114
appendedCollectedData: []string{
115
"0123456789012345678901234567890123456789012345678901\n",
116
"the quick brown fox jumps over the lazy dog\n",
119
description: "missing termination of last line",
121
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
122
"0123456789012345678901234567890123456789012345678901\n",
123
"the quick brown fox ",
125
initialLinesWritten: 1,
126
initialLinesRequested: 1,
127
initialCollectedData: []string{
128
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
130
appendedCollectedData: []string{
131
"0123456789012345678901234567890123456789012345678901\n",
134
description: "last line is terminated later",
136
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
137
"0123456789012345678901234567890123456789012345678901\n",
138
"the quick brown fox ",
139
"jumps over the lazy dog\n",
141
initialLinesWritten: 1,
142
initialLinesRequested: 1,
143
initialCollectedData: []string{
144
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\n",
146
appendedCollectedData: []string{
147
"0123456789012345678901234567890123456789012345678901\n",
148
"the quick brown fox jumps over the lazy dog\n",
151
description: "more lines already written than initially requested",
153
initialLinesWritten: 5,
154
initialLinesRequested: 3,
155
initialCollectedData: []string{
160
appendedCollectedData: alphabetData[5:],
162
description: "less lines already written than initially requested",
164
initialLinesWritten: 3,
165
initialLinesRequested: 5,
166
initialCollectedData: []string{
171
appendedCollectedData: alphabetData[3:],
173
description: "lines are longer than buffer size, more lines already written than initially requested",
175
initialLinesWritten: 5,
176
initialLinesRequested: 3,
178
initialCollectedData: []string{
183
appendedCollectedData: alphabetData[5:],
185
description: "lines are longer than buffer size, less lines already written than initially requested",
187
initialLinesWritten: 3,
188
initialLinesRequested: 5,
190
initialCollectedData: []string{
195
appendedCollectedData: alphabetData[3:],
197
description: "filter lines which contain the char 'e'",
199
initialLinesWritten: 10,
200
initialLinesRequested: 3,
201
filter: func(line []byte) bool {
202
return bytes.Contains(line, []byte{'e'})
204
initialCollectedData: []string{
209
appendedCollectedData: []string{
211
"november november\n",
219
description: "stop tailing after 10 collected lines",
221
initialLinesWritten: 5,
222
initialLinesRequested: 3,
223
injector: func(t *tailer.Tailer, rs *readSeeker) func([]string) {
224
return func(lines []string) {
225
if len(lines) == 10 {
230
initialCollectedData: []string{
235
appendedCollectedData: alphabetData[5:],
237
description: "generate an error after 10 collected lines",
239
initialLinesWritten: 5,
240
initialLinesRequested: 3,
241
injector: func(t *tailer.Tailer, rs *readSeeker) func([]string) {
242
return func(lines []string) {
243
if len(lines) == 10 {
244
rs.setError(fmt.Errorf("ouch after 10 lines"))
248
initialCollectedData: []string{
253
appendedCollectedData: alphabetData[5:],
254
err: "ouch after 10 lines",
256
description: "more lines already written than initially requested, some empty, unfiltered",
269
initialLinesWritten: 3,
270
initialLinesRequested: 2,
271
initialCollectedData: []string{
275
appendedCollectedData: []string{
285
description: "more lines already written than initially requested, some empty, those filtered",
298
initialLinesWritten: 3,
299
initialLinesRequested: 2,
300
filter: func(line []byte) bool {
301
return len(bytes.TrimSpace(line)) > 0
303
initialCollectedData: []string{
307
appendedCollectedData: []string{
315
func (tailerSuite) TestTailer(c *gc.C) {
316
for i, test := range tests {
317
c.Logf("Test #%d) %s", i, test.description)
318
bufferSize := test.bufferSize
323
reader, writer := io.Pipe()
324
sigc := make(chan struct{}, 1)
325
rs := startReadSeeker(c, test.data, test.initialLinesWritten, sigc)
326
tailer := tailer.NewTestTailer(rs, writer, test.initialLinesRequested, test.filter, bufferSize, 2*time.Millisecond)
327
linec := startReading(c, tailer, reader, writer)
329
// Collect initial data.
330
assertCollected(c, linec, test.initialCollectedData, nil)
334
// Collect remaining data, possibly with injection to stop
335
// earlier or generate an error.
336
var injection func([]string)
337
if test.injector != nil {
338
injection = test.injector(tailer, rs)
341
assertCollected(c, linec, test.appendedCollectedData, injection)
344
c.Assert(tailer.Stop(), gc.IsNil)
346
c.Assert(tailer.Err(), gc.ErrorMatches, test.err)
351
// startReading starts a goroutine receiving the lines out of the reader
352
// in the background and passing them to a created string channel. This
353
// will used in the assertions.
354
func startReading(c *gc.C, tailer *tailer.Tailer, reader *io.PipeReader, writer *io.PipeWriter) chan string {
355
linec := make(chan string)
356
// Start goroutine for reading.
359
reader := bufio.NewReader(reader)
361
line, err := reader.ReadString('\n')
372
// Close writer when tailer is stopped or has an error. Tailer using
373
// components can do it the same way.
381
// assertCollected reads lines from the string channel linec. It compares if
382
// those are the one passed with compare until a timeout. If the timeout is
383
// reached earlier than all lines are collected the assertion fails. The
384
// injection function allows to interrupt the processing with a function
385
// generating an error or a regular stopping during the tailing. In case the
386
// linec is closed due to stopping or an error only the values so far care
387
// compared. Checking the reason for termination is done in the test.
388
func assertCollected(c *gc.C, linec chan string, compare []string, injection func([]string)) {
389
timeout := time.After(testing.LongWait)
393
case line, ok := <-linec:
395
lines = append(lines, line)
396
if injection != nil {
399
if len(lines) == len(compare) {
400
// All data received.
401
c.Assert(lines, gc.DeepEquals, compare)
405
// linec closed after stopping or error.
406
c.Assert(lines, gc.DeepEquals, compare[:len(lines)])
410
if injection == nil {
411
c.Fatalf("timeout during tailer collection")
418
// startReadSeeker returns a ReadSeeker for the Tailer. It simulates
419
// reading and seeking inside a file and also simulating an error.
420
// The goroutine waits for a signal that it can start writing the
422
func startReadSeeker(c *gc.C, data []string, initialLeg int, sigc chan struct{}) *readSeeker {
423
// Write initial lines into the buffer.
426
for i = 0; i < initialLeg; i++ {
433
for ; i < len(data); i++ {
434
time.Sleep(5 * time.Millisecond)
441
type readSeeker struct {
448
func (r *readSeeker) write(s string) {
451
r.buffer = append(r.buffer, []byte(s)...)
454
func (r *readSeeker) setError(err error) {
460
func (r *readSeeker) Read(p []byte) (n int, err error) {
466
if r.pos >= len(r.buffer) {
469
n = copy(p, r.buffer[r.pos:])
474
func (r *readSeeker) Seek(offset int64, whence int) (ret int64, err error) {
482
newPos = int64(r.pos) + offset
484
newPos = int64(len(r.buffer)) + offset
486
return 0, fmt.Errorf("invalid whence: %d", whence)
489
return 0, fmt.Errorf("negative position: %d", newPos)
492
return 0, fmt.Errorf("position out of range: %d", newPos)