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  }