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