github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/hud/server/logs_reader_test.go (about) 1 package server 2 3 import ( 4 "bytes" 5 "fmt" 6 "strings" 7 "testing" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 12 "github.com/tilt-dev/tilt/pkg/model" 13 14 "github.com/tilt-dev/tilt/internal/hud" 15 proto_webview "github.com/tilt-dev/tilt/pkg/webview" 16 ) 17 18 var alphabet = []string{"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", 19 "golf", "hotel", "igloo", "juliette", "kilo", "lima", "mike", "november", 20 "oscar", "papa", "quebec", "romeo", "sierra", "tango", "uniform", "victor", 21 "whiskey", "xavier", "yankee", "zulu"} 22 23 type expectedLine struct { 24 prefix string // ~manifestName (leave blank for "global" / unprefixed) 25 message string 26 } 27 28 func TestLogStreamerPrintsLogs(t *testing.T) { 29 f := newLogStreamerFixture(t) 30 31 view := f.newViewWithLogsForManifest(alphabet[:4], "foo", 0) 32 f.handle(view) 33 34 expected := f.expectedLinesWithPrefix(alphabet[:4], "foo") 35 36 f.assertExpectedLogLines(expected) 37 } 38 39 func TestHandleEmptyView(t *testing.T) { 40 f := newLogStreamerFixture(t) 41 f.handle(&proto_webview.View{}) 42 f.assertExpectedLogLines([]expectedLine{expectedLine{}}) // Always end in a newline 43 } 44 45 func TestLogStreamerPrefixing(t *testing.T) { 46 f := newLogStreamerFixture(t) 47 manifestNames := []string{"foo", "", "foo", "bar"} 48 49 view := f.newViewWithLogsForManifests(alphabet[:4], manifestNames, 0) 50 f.handle(view) 51 52 expected := f.expectedLinesWithPrefixes(alphabet[:4], manifestNames) 53 54 f.assertExpectedLogLines(expected) 55 } 56 57 func TestLogStreamerDoesNotPrintResentLogs(t *testing.T) { 58 f := newLogStreamerFixture(t) 59 60 view := f.newViewWithLogsForManifest(alphabet[:4], "foo", 0) 61 f.handle(view) 62 63 view = f.newViewWithLogsForManifest(alphabet[:5], "foo", 0) 64 f.handle(view) 65 66 expected := f.expectedLinesWithPrefix(alphabet[:5], "foo") 67 68 f.assertExpectedLogLines(expected) 69 } 70 71 func TestLogStreamerCheckpointHandling(t *testing.T) { 72 // the client might attach after the server has truncated, so the first "FromCheckpoint" it sees 73 // won't always be 0 74 for _, initialOffset := range []int32{0, 1000} { 75 t.Run(fmt.Sprintf("Offset-%d", initialOffset), func(t *testing.T) { 76 f := newLogStreamerFixture(t) 77 resourceNames := []string{ 78 "foo", "", "foo", "bar", 79 "bar", "bar", "", "bar", 80 "", "foo", "bar", "baz"} 81 view := f.newViewWithLogsForManifests(alphabet[:4], resourceNames[:4], initialOffset) 82 f.handle(view) 83 84 view = f.newViewWithLogsForManifests(alphabet[4:8], resourceNames[4:8], view.LogList.ToCheckpoint) 85 f.handle(view) 86 87 view = f.newViewWithLogsForManifests(alphabet[8:12], resourceNames[8:12], view.LogList.ToCheckpoint) 88 f.handle(view) 89 90 expected := f.expectedLinesWithPrefixes(alphabet[:12], resourceNames[:12]) 91 f.assertExpectedLogLines(expected) 92 }) 93 } 94 } 95 96 func TestLogStreamerFiltersOnResourceNamesSingle(t *testing.T) { 97 f := newLogStreamerFixture(t).withResourceNames("foo") 98 manifestNames := []string{"foo", "", "foo", "bar"} 99 view := f.newViewWithLogsForManifests(alphabet[:4], manifestNames, 0) 100 f.handle(view) 101 102 // Expect no prefix b/c we're filtering for a single resource, so prefixing is redundant 103 expected := f.expectedLinesWithPrefix([]string{"alpha", "charlie"}, "") 104 f.assertExpectedLogLines(expected) 105 } 106 107 func TestLogStreamerFiltersOnResourceNamesMultiple(t *testing.T) { 108 f := newLogStreamerFixture(t).withResourceNames("foo", "baz") 109 manifestNames := []string{"foo", "", "foo", "bar", "baz", "bar", "baz", "foo"} 110 view := f.newViewWithLogsForManifests(alphabet[:8], manifestNames, 0) 111 f.handle(view) 112 113 expected := f.expectedLinesWithPrefixes( 114 []string{"alpha", "charlie", "echo", "golf", "hotel"}, []string{"foo", "foo", "baz", "baz", "foo"}) 115 f.assertExpectedLogLines(expected) 116 } 117 118 func TestLogStreamerCheckpointHandlingWithFiltering(t *testing.T) { 119 f := newLogStreamerFixture(t).withResourceNames("foo", "baz") 120 view := f.newViewWithLogsForManifests(alphabet[:4], []string{"foo", "", "foo", "bar"}, 0) 121 f.handle(view) 122 123 view = f.newViewWithLogsForManifests(alphabet[4:8], []string{"bar", "bar", "", "bar"}, view.LogList.ToCheckpoint) 124 f.handle(view) 125 126 view = f.newViewWithLogsForManifests(alphabet[8:12], []string{"", "foo", "bar", "baz"}, view.LogList.ToCheckpoint) 127 f.handle(view) 128 129 expected := f.expectedLinesWithPrefixes( 130 []string{"alpha", "charlie", "juliette", "lima"}, []string{"foo", "foo", "foo", "baz"}) 131 f.assertExpectedLogLines(expected) 132 } 133 134 type logStreamerFixture struct { 135 t *testing.T 136 fakeStdout *bytes.Buffer 137 printer *hud.IncrementalPrinter 138 ls *LogStreamer 139 } 140 141 func newLogStreamerFixture(t *testing.T) *logStreamerFixture { 142 fakeStdout := &bytes.Buffer{} 143 printer := hud.NewIncrementalPrinter(hud.Stdout(fakeStdout)) 144 return &logStreamerFixture{ 145 t: t, 146 fakeStdout: fakeStdout, 147 printer: printer, 148 ls: NewLogStreamer(nil, printer), 149 } 150 } 151 152 func (f *logStreamerFixture) withResourceNames(resourceNames ...string) *logStreamerFixture { 153 rns := make(model.ManifestNameSet, len(resourceNames)) 154 for _, rn := range resourceNames { 155 rns[model.ManifestName(rn)] = true 156 } 157 f.ls.resources = rns 158 return f 159 } 160 161 func (f *logStreamerFixture) handle(view *proto_webview.View) { 162 err := f.ls.Handle(view) 163 require.NoError(f.t, err) 164 } 165 166 func (f *logStreamerFixture) newViewWithLogsForManifest(messages []string, manifestName string, fromChkpt int32) *proto_webview.View { 167 dummyManifestNames := make([]string, len(messages)) 168 for i := 0; i < len(messages); i++ { 169 dummyManifestNames[i] = manifestName 170 } 171 return f.newViewWithLogsForManifests(messages, dummyManifestNames, fromChkpt) 172 } 173 174 func (f *logStreamerFixture) newViewWithLogsForManifests(messages []string, manifestNames []string, fromChkpt int32) *proto_webview.View { 175 segs := f.segments(messages, manifestNames) 176 spans := f.spans(manifestNames, nil) 177 178 return &proto_webview.View{ 179 LogList: &proto_webview.LogList{ 180 Spans: spans, 181 Segments: segs, 182 FromCheckpoint: fromChkpt, 183 ToCheckpoint: fromChkpt + int32(len(segs)), 184 }, 185 } 186 } 187 188 func (f *logStreamerFixture) segments(messages []string, manifestNames []string) []*proto_webview.LogSegment { 189 if len(messages) != len(manifestNames) { 190 f.t.Fatalf("Need same number of messages and manifestNames (got %d and %d)", 191 len(messages), len(manifestNames)) 192 } 193 194 segs := make([]*proto_webview.LogSegment, len(messages)) 195 for i, msg := range messages { 196 segs[i] = &proto_webview.LogSegment{ 197 SpanId: spanID(manifestNames[i]), 198 Text: msg + "\n", 199 Level: 0, // TODO 200 } 201 } 202 203 return segs 204 } 205 206 func (f *logStreamerFixture) spans(manifestNames []string, 207 existingSpans map[string]*proto_webview.LogSpan) map[string]*proto_webview.LogSpan { 208 209 if existingSpans == nil { 210 existingSpans = make(map[string]*proto_webview.LogSpan) 211 } 212 213 for _, mn := range manifestNames { 214 existingSpans[spanID(mn)] = &proto_webview.LogSpan{ManifestName: mn} 215 } 216 217 return existingSpans 218 } 219 220 func (f *logStreamerFixture) assertExpectedLogLines(expectedLines []expectedLine) { 221 out := strings.TrimRight(f.fakeStdout.String(), "\n") 222 outLines := strings.Split(out, "\n") 223 if len(outLines) != len(expectedLines) { 224 f.t.Errorf("Expected %d log lines but got %d", len(expectedLines), len(outLines)) 225 fmt.Printf("=== Test failed with logs ===\n%s\n", out) 226 f.t.FailNow() 227 } 228 229 for i, ln := range outLines { 230 lnTrimmed := strings.TrimSpace(ln) 231 expected := expectedLines[i] 232 assert.True(f.t, strings.Contains(lnTrimmed, expected.message), 233 "expect message %q in line: %q", expected.message, ln) 234 if expected.prefix != "" { 235 assert.True(f.t, strings.HasPrefix(lnTrimmed, expected.prefix), 236 "expect prefix %q in line: %q", expected.prefix, lnTrimmed) 237 } else { 238 // Expect no prefix 239 assert.False(f.t, strings.Contains(lnTrimmed, "|"), 240 "expect no prefix but found \"|\" in line: %q", lnTrimmed) 241 } 242 } 243 244 if f.t.Failed() { 245 fmt.Printf("=== Test failed with logs ===\n%s\n", out) 246 } 247 } 248 249 func (f *logStreamerFixture) expectedLinesWithPrefix(messages []string, prefix string) []expectedLine { 250 expected := make([]expectedLine, len(messages)) 251 for i, msg := range messages { 252 expected[i] = expectedLine{prefix, msg} 253 } 254 return expected 255 } 256 257 func (f *logStreamerFixture) expectedLinesWithPrefixes(messages []string, prefixes []string) []expectedLine { 258 if len(prefixes) != len(messages) { 259 f.t.Fatalf("Need same number of prefixes and messages (got %d and %d)", 260 len(prefixes), len(messages)) 261 } 262 expected := make([]expectedLine, len(messages)) 263 for i, msg := range messages { 264 expected[i] = expectedLine{prefixes[i], msg} 265 } 266 return expected 267 } 268 269 func spanID(mn string) string { 270 return fmt.Sprintf("spanID-%s", mn) 271 }