| package tailfile // import "github.com/docker/docker/pkg/tailfile" | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "context" | |
| "fmt" | |
| "io" | |
| "os" | |
| "strings" | |
| "testing" | |
| "gotest.tools/v3/assert" | |
| ) | |
| func TestTailFile(t *testing.T) { | |
| f, err := os.CreateTemp("", "tail-test") | |
| 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) | |
| } | |
| if _, err := f.Seek(0, io.SeekStart); err != nil { | |
| t.Fatal(err) | |
| } | |
| expected := []string{"last fourth line", "last fifth line"} | |
| res, err := TailFile(f, 2) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(res) != len(expected) { | |
| t.Fatalf("\nexpected:\n%s\n\nactual:\n%s", expected, res) | |
| } | |
| for i, l := range res { | |
| if expected[i] != string(l) { | |
| t.Fatalf("Expected line %q, got %q", expected[i], l) | |
| } | |
| } | |
| } | |
| func TestTailFileManyLines(t *testing.T) { | |
| f, err := os.CreateTemp("", "tail-test") | |
| 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) | |
| } | |
| if _, err := f.Seek(0, io.SeekStart); err != nil { | |
| t.Fatal(err) | |
| } | |
| expected := []string{"first line", "second line"} | |
| res, err := TailFile(f, 10000) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(expected) != len(res) { | |
| t.Fatalf("\nexpected:\n%s\n\nactual:\n%s", expected, res) | |
| } | |
| 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) { | |
| f, err := os.CreateTemp("", "tail-test") | |
| 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) { | |
| f, err := os.CreateTemp("", "tail-test") | |
| 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) | |
| } | |
| if _, err := f.Seek(0, io.SeekStart); err != nil { | |
| t.Fatal(err) | |
| } | |
| if _, err := TailFile(f, -1); err != ErrNonPositiveLinesNumber { | |
| t.Fatalf("Expected ErrNonPositiveLinesNumber, got %v", err) | |
| } | |
| if _, err := TailFile(f, 0); err != ErrNonPositiveLinesNumber { | |
| t.Fatalf("Expected ErrNonPositiveLinesNumber, got %s", err) | |
| } | |
| } | |
| func BenchmarkTail(b *testing.B) { | |
| f, err := os.CreateTemp("", "tail-test") | |
| 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) | |
| } | |
| } | |
| } | |
| 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() | |
| maxLen := len(test.data) | |
| if maxLen > 10 { | |
| maxLen = 10 | |
| } | |
| s := strings.Join(test.data, string(delim)) | |
| if len(test.data) > 0 { | |
| s += string(delim) | |
| } | |
| for i := 1; i <= maxLen; 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 | |
| } | |
| assert.NilError(t, err) | |
| assert.Check(t, lines == i, "%d -- %d", lines, i) | |
| b, err := io.ReadAll(tr) | |
| assert.NilError(t, err) | |
| 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 | |
| } | |
| assert.NilError(t, err) | |
| assert.Check(t, lines == len(test.data), "%d -- %d", lines, len(test.data)) | |
| b, err := io.ReadAll(tr) | |
| assert.NilError(t, err) | |
| 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) | |
| assert.NilError(t, err) | |
| assert.Check(t, nLines == 2, nLines) | |
| rdr := bufio.NewReader(tail) | |
| data, _, err := rdr.ReadLine() | |
| assert.NilError(t, err) | |
| assert.Check(t, string(data) == "a", string(data)) | |
| data, _, err = rdr.ReadLine() | |
| assert.NilError(t, err) | |
| 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) | |
| assert.NilError(t, err) | |
| assert.Check(t, nLines == 2, nLines) | |
| rdr := bufio.NewReader(tail) | |
| data, _, err := rdr.ReadLine() | |
| assert.NilError(t, err) | |
| assert.Check(t, string(data) == "a", string(data)) | |
| data, _, err = rdr.ReadLine() | |
| assert.NilError(t, err) | |
| 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) | |
| assert.NilError(t, err) | |
| assert.Check(t, nLines == 1, nLines) | |
| rdr := bufio.NewReader(tail) | |
| data, _, err := rdr.ReadLine() | |
| assert.NilError(t, err) | |
| assert.Check(t, string(data) == "b", string(data)) | |
| _, _, err = rdr.ReadLine() | |
| assert.Assert(t, err == io.EOF, err) | |
| }) | |
| }) | |
| } |