2018-02-05 16:05:59 -05:00
|
|
|
package tailfile // import "github.com/docker/docker/pkg/tailfile"
|
2014-06-03 07:09:33 -04:00
|
|
|
|
|
|
|
import (
|
2018-04-05 12:38:06 -04:00
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2014-06-03 07:09:33 -04:00
|
|
|
"os"
|
2018-04-05 12:38:06 -04:00
|
|
|
"strings"
|
2014-06-03 07:09:33 -04:00
|
|
|
"testing"
|
2018-04-05 12:38:06 -04:00
|
|
|
|
2020-02-07 08:39:24 -05:00
|
|
|
"gotest.tools/v3/assert"
|
2014-06-03 07:09:33 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestTailFile(t *testing.T) {
|
2021-08-24 06:10:50 -04:00
|
|
|
f, err := os.CreateTemp("", "tail-test")
|
2014-06-03 07:09:33 -04:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
defer os.RemoveAll(f.Name())
|
|
|
|
testFile := []byte(`first line
|
|
|
|
second line
|
|
|
|
third line
|
|
|
|
fourth line
|
|
|
|
fifth line
|
|
|
|
next first line
|
|
|
|
next second line
|
|
|
|
next third line
|
|
|
|
next fourth line
|
|
|
|
next fifth line
|
|
|
|
last first line
|
|
|
|
next first line
|
|
|
|
next second line
|
|
|
|
next third line
|
|
|
|
next fourth line
|
|
|
|
next fifth line
|
|
|
|
next first line
|
|
|
|
next second line
|
|
|
|
next third line
|
|
|
|
next fourth line
|
|
|
|
next fifth line
|
|
|
|
last second line
|
|
|
|
last third line
|
|
|
|
last fourth line
|
|
|
|
last fifth line
|
|
|
|
truncated line`)
|
|
|
|
if _, err := f.Write(testFile); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2018-04-05 12:38:06 -04:00
|
|
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
2014-06-03 07:09:33 -04:00
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
expected := []string{"last fourth line", "last fifth line"}
|
|
|
|
res, err := TailFile(f, 2)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2018-04-05 12:38:06 -04:00
|
|
|
if len(res) != len(expected) {
|
|
|
|
t.Fatalf("\nexpected:\n%s\n\nactual:\n%s", expected, res)
|
|
|
|
}
|
2014-06-03 07:09:33 -04:00
|
|
|
for i, l := range res {
|
|
|
|
if expected[i] != string(l) {
|
2018-04-05 12:38:06 -04:00
|
|
|
t.Fatalf("Expected line %q, got %q", expected[i], l)
|
2014-06-03 07:09:33 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestTailFileManyLines(t *testing.T) {
|
2021-08-24 06:10:50 -04:00
|
|
|
f, err := os.CreateTemp("", "tail-test")
|
2014-06-03 07:09:33 -04:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
defer os.RemoveAll(f.Name())
|
|
|
|
testFile := []byte(`first line
|
|
|
|
second line
|
|
|
|
truncated line`)
|
|
|
|
if _, err := f.Write(testFile); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2018-04-05 12:38:06 -04:00
|
|
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
2014-06-03 07:09:33 -04:00
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
expected := []string{"first line", "second line"}
|
|
|
|
res, err := TailFile(f, 10000)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2018-04-05 12:38:06 -04:00
|
|
|
if len(expected) != len(res) {
|
|
|
|
t.Fatalf("\nexpected:\n%s\n\nactual:\n%s", expected, res)
|
|
|
|
}
|
2014-06-03 07:09:33 -04:00
|
|
|
for i, l := range res {
|
|
|
|
if expected[i] != string(l) {
|
|
|
|
t.Fatalf("Expected line %s, got %s", expected[i], l)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestTailEmptyFile(t *testing.T) {
|
2021-08-24 06:10:50 -04:00
|
|
|
f, err := os.CreateTemp("", "tail-test")
|
2014-06-03 07:09:33 -04:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
defer os.RemoveAll(f.Name())
|
|
|
|
res, err := TailFile(f, 10000)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if len(res) != 0 {
|
|
|
|
t.Fatal("Must be empty slice from empty file")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestTailNegativeN(t *testing.T) {
|
2021-08-24 06:10:50 -04:00
|
|
|
f, err := os.CreateTemp("", "tail-test")
|
2014-06-03 07:09:33 -04:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
defer os.RemoveAll(f.Name())
|
|
|
|
testFile := []byte(`first line
|
|
|
|
second line
|
|
|
|
truncated line`)
|
|
|
|
if _, err := f.Write(testFile); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2018-04-05 12:38:06 -04:00
|
|
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
2014-06-03 07:09:33 -04:00
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if _, err := TailFile(f, -1); err != ErrNonPositiveLinesNumber {
|
2018-04-05 12:38:06 -04:00
|
|
|
t.Fatalf("Expected ErrNonPositiveLinesNumber, got %v", err)
|
2014-06-03 07:09:33 -04:00
|
|
|
}
|
|
|
|
if _, err := TailFile(f, 0); err != ErrNonPositiveLinesNumber {
|
|
|
|
t.Fatalf("Expected ErrNonPositiveLinesNumber, got %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func BenchmarkTail(b *testing.B) {
|
2021-08-24 06:10:50 -04:00
|
|
|
f, err := os.CreateTemp("", "tail-test")
|
2014-06-03 07:09:33 -04:00
|
|
|
if err != nil {
|
|
|
|
b.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
defer os.RemoveAll(f.Name())
|
|
|
|
for i := 0; i < 10000; i++ {
|
|
|
|
if _, err := f.Write([]byte("tailfile pretty interesting line\n")); err != nil {
|
|
|
|
b.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
if _, err := TailFile(f, 1000); err != nil {
|
|
|
|
b.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-05 12:38:06 -04:00
|
|
|
|
|
|
|
func TestNewTailReader(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
for dName, delim := range map[string][]byte{
|
|
|
|
"no delimiter": {},
|
|
|
|
"single byte delimiter": {'\n'},
|
|
|
|
"2 byte delimiter": []byte(";\n"),
|
|
|
|
"4 byte delimiter": []byte("####"),
|
|
|
|
"8 byte delimiter": []byte("########"),
|
|
|
|
"12 byte delimiter": []byte("############"),
|
|
|
|
} {
|
|
|
|
t.Run(dName, func(t *testing.T) {
|
|
|
|
delim := delim
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
s1 := "Hello world."
|
|
|
|
s2 := "Today is a fine day."
|
|
|
|
s3 := "So long, and thanks for all the fish!"
|
|
|
|
s4 := strings.Repeat("a", blockSize/2) // same as block size
|
|
|
|
s5 := strings.Repeat("a", blockSize) // just to make sure
|
|
|
|
s6 := strings.Repeat("a", blockSize*2) // bigger than block size
|
|
|
|
s7 := strings.Repeat("a", blockSize-1) // single line same as block
|
|
|
|
|
|
|
|
s8 := `{"log":"Don't panic!\n","stream":"stdout","time":"2018-04-04T20:28:44.7207062Z"}`
|
|
|
|
jsonTest := make([]string, 0, 20)
|
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
jsonTest = append(jsonTest, s8)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range []struct {
|
|
|
|
desc string
|
|
|
|
data []string
|
|
|
|
}{
|
|
|
|
{desc: "one small entry", data: []string{s1}},
|
|
|
|
{desc: "several small entries", data: []string{s1, s2, s3}},
|
|
|
|
{desc: "various sizes", data: []string{s1, s2, s3, s4, s5, s1, s2, s3, s7, s6}},
|
|
|
|
{desc: "multiple lines with one more than block", data: []string{s5, s5, s5, s5, s5}},
|
|
|
|
{desc: "multiple lines much bigger than block", data: []string{s6, s6, s6, s6, s6}},
|
|
|
|
{desc: "multiple lines same as block", data: []string{s4, s4, s4, s4, s4}},
|
|
|
|
{desc: "single line same as block", data: []string{s7}},
|
|
|
|
{desc: "single line half block", data: []string{s4}},
|
|
|
|
{desc: "single line twice block", data: []string{s6}},
|
|
|
|
{desc: "json encoded values", data: jsonTest},
|
|
|
|
{desc: "no lines", data: []string{}},
|
|
|
|
{desc: "same length as delimiter", data: []string{strings.Repeat("a", len(delim))}},
|
|
|
|
} {
|
|
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
|
|
test := test
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
max := len(test.data)
|
|
|
|
if max > 10 {
|
|
|
|
max = 10
|
|
|
|
}
|
|
|
|
|
|
|
|
s := strings.Join(test.data, string(delim))
|
|
|
|
if len(test.data) > 0 {
|
|
|
|
s += string(delim)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := 1; i <= max; i++ {
|
|
|
|
t.Run(fmt.Sprintf("%d lines", i), func(t *testing.T) {
|
|
|
|
i := i
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
r := strings.NewReader(s)
|
|
|
|
tr, lines, err := NewTailReaderWithDelimiter(ctx, r, i, delim)
|
|
|
|
if len(delim) == 0 {
|
|
|
|
assert.Assert(t, err != nil)
|
|
|
|
assert.Assert(t, lines == 0)
|
|
|
|
return
|
|
|
|
}
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, lines == i, "%d -- %d", lines, i)
|
|
|
|
|
2021-08-24 06:10:50 -04:00
|
|
|
b, err := io.ReadAll(tr)
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
|
|
|
|
expectLines := test.data[len(test.data)-i:]
|
|
|
|
assert.Check(t, len(expectLines) == i)
|
|
|
|
expect := strings.Join(expectLines, string(delim)) + string(delim)
|
|
|
|
assert.Check(t, string(b) == expect, "\n%v\n%v", b, []byte(expect))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Run("request more lines than available", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
r := strings.NewReader(s)
|
|
|
|
tr, lines, err := NewTailReaderWithDelimiter(ctx, r, len(test.data)*2, delim)
|
|
|
|
if len(delim) == 0 {
|
|
|
|
assert.Assert(t, err != nil)
|
|
|
|
assert.Assert(t, lines == 0)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(test.data) == 0 {
|
|
|
|
assert.Assert(t, err == ErrNonPositiveLinesNumber, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, lines == len(test.data), "%d -- %d", lines, len(test.data))
|
2021-08-24 06:10:50 -04:00
|
|
|
b, err := io.ReadAll(tr)
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, bytes.Equal(b, []byte(s)), "\n%v\n%v", b, []byte(s))
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
t.Run("truncated last line", func(t *testing.T) {
|
|
|
|
t.Run("more than available", func(t *testing.T) {
|
|
|
|
tail, nLines, err := NewTailReader(ctx, strings.NewReader("a\nb\nextra"), 3)
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, nLines == 2, nLines)
|
|
|
|
|
|
|
|
rdr := bufio.NewReader(tail)
|
|
|
|
data, _, err := rdr.ReadLine()
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, string(data) == "a", string(data))
|
|
|
|
|
|
|
|
data, _, err = rdr.ReadLine()
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, string(data) == "b", string(data))
|
|
|
|
|
|
|
|
_, _, err = rdr.ReadLine()
|
|
|
|
assert.Assert(t, err == io.EOF, err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
t.Run("truncated last line", func(t *testing.T) {
|
|
|
|
t.Run("exact", func(t *testing.T) {
|
|
|
|
tail, nLines, err := NewTailReader(ctx, strings.NewReader("a\nb\nextra"), 2)
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, nLines == 2, nLines)
|
|
|
|
|
|
|
|
rdr := bufio.NewReader(tail)
|
|
|
|
data, _, err := rdr.ReadLine()
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, string(data) == "a", string(data))
|
|
|
|
|
|
|
|
data, _, err = rdr.ReadLine()
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, string(data) == "b", string(data))
|
|
|
|
|
|
|
|
_, _, err = rdr.ReadLine()
|
|
|
|
assert.Assert(t, err == io.EOF, err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("truncated last line", func(t *testing.T) {
|
|
|
|
t.Run("one line", func(t *testing.T) {
|
|
|
|
tail, nLines, err := NewTailReader(ctx, strings.NewReader("a\nb\nextra"), 1)
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, nLines == 1, nLines)
|
|
|
|
|
|
|
|
rdr := bufio.NewReader(tail)
|
|
|
|
data, _, err := rdr.ReadLine()
|
2019-01-21 07:16:02 -05:00
|
|
|
assert.NilError(t, err)
|
2018-04-05 12:38:06 -04:00
|
|
|
assert.Check(t, string(data) == "b", string(data))
|
|
|
|
|
|
|
|
_, _, err = rdr.ReadLine()
|
|
|
|
assert.Assert(t, err == io.EOF, err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|