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  }