1package swarm
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "path/filepath"
8 "testing"
9)
10
11// writeTailLog writes n events "e0".."e<n-1>" and returns the path and
12// the file size.
13func writeTailLog(t *testing.T, n int) (string, int64) {
14 t.Helper()
15 path := filepath.Join(t.TempDir(), "events.jsonl")
16 log, err := OpenEventLog(path)
17 if err != nil {
18 t.Fatalf("open: %v", err)
19 }
20 for i := 0; i < n; i++ {
21 if err := log.Append(NewEvent(fmt.Sprintf("e%d", i), map[string]any{"pad": "0123456789"})); err != nil {
22 t.Fatalf("append: %v", err)
23 }
24 }
25 _ = log.Close()
26 info, err := os.Stat(path)
27 if err != nil {
28 t.Fatalf("stat: %v", err)
29 }
30 return path, info.Size()
31}
32
33// lineStarts returns the byte offset of every line in the file. Event
34// lines differ in length (timestamps drop trailing zeros), so windows
35// are computed from real offsets rather than an average line size.
36func lineStarts(t *testing.T, path string) []int64 {
37 t.Helper()
38 data, err := os.ReadFile(path)
39 if err != nil {
40 t.Fatalf("read: %v", err)
41 }
42 starts := []int64{0}
43 for i, b := range data[:len(data)-1] {
44 if b == '\n' {
45 starts = append(starts, int64(i+1))
46 }
47 }
48 if !bytes.HasSuffix(data, []byte("\n")) {
49 t.Fatalf("log does not end with a newline")
50 }
51 return starts
52}
53
54func eventTypes(evs []Event) []string {
55 out := make([]string, len(evs))
56 for i, ev := range evs {
57 out[i] = ev.Type
58 }
59 return out
60}
61
62func TestReadEventLogTailSmallFileReadsEverything(t *testing.T) {
63 path, size := writeTailLog(t, 5)
64 for _, limit := range []int64{0, size, size + 100} {
65 got, err := ReadEventLogTail(path, limit)
66 if err != nil {
67 t.Fatalf("limit %d: %v", limit, err)
68 }
69 if len(got) != 5 || got[0].Type != "e0" || got[4].Type != "e4" {
70 t.Fatalf("limit %d: got %v", limit, eventTypes(got))
71 }
72 }
73}
74
75// The tail window usually starts mid-line; that partial line must be
76// dropped rather than parsed, and every later event returned in order.
77func TestReadEventLogTailSkipsPartialFirstLine(t *testing.T) {
78 path, size := writeTailLog(t, 50)
79 starts := lineStarts(t, path)
80 // Start the window in the middle of line 46: e46 is partial.
81 mid := (starts[46] + starts[47]) / 2
82 got, err := ReadEventLogTail(path, size-mid)
83 if err != nil {
84 t.Fatalf("read: %v", err)
85 }
86 want := []string{"e47", "e48", "e49"}
87 if fmt.Sprint(eventTypes(got)) != fmt.Sprint(want) {
88 t.Fatalf("got %v, want %v", eventTypes(got), want)
89 }
90}
91
92// A window starting on a line boundary must keep its first event.
93// In particular, that event might be the only lifecycle marker in the tail.
94func TestReadEventLogTailBoundaryWindow(t *testing.T) {
95 path, size := writeTailLog(t, 10)
96 starts := lineStarts(t, path)
97 // The window starts exactly at the beginning of e6.
98 got, err := ReadEventLogTail(path, size-starts[6])
99 if err != nil {
100 t.Fatalf("read: %v", err)
101 }
102 want := []string{"e6", "e7", "e8", "e9"}
103 if fmt.Sprint(eventTypes(got)) != fmt.Sprint(want) {
104 t.Fatalf("got %v, want %v", eventTypes(got), want)
105 }
106}
107
108func TestReadEventLogTailWindowWithoutNewline(t *testing.T) {
109 path, _ := writeTailLog(t, 10)
110 got, err := ReadEventLogTail(path, 5) // inside the last line
111 if err != nil || len(got) != 0 {
112 t.Fatalf("got %v, %v; want no events and no error", eventTypes(got), err)
113 }
114}
115
116func TestReadEventLogTailMissingFile(t *testing.T) {
117 got, err := ReadEventLogTail(filepath.Join(t.TempDir(), "missing.jsonl"), 1024)
118 if err != nil || got != nil {
119 t.Fatalf("got %v, %v; want nil, nil", got, err)
120 }
121}