github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/telemetry/controller_test.go (about)

     1  package telemetry
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"runtime"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"go.opentelemetry.io/otel/sdk/trace"
    16  	"go.opentelemetry.io/otel/sdk/trace/tracetest"
    17  
    18  	"github.com/tilt-dev/tilt/internal/store"
    19  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    20  	"github.com/tilt-dev/tilt/internal/tracer"
    21  	"github.com/tilt-dev/tilt/pkg/model"
    22  )
    23  
    24  func TestTelNoScriptTimeIsUpNoInvocation(t *testing.T) {
    25  	f := newTCFixture(t)
    26  
    27  	f.run()
    28  
    29  	f.assertNoInvocation()
    30  }
    31  
    32  func TestTelNoScriptTimeIsNotUpNoInvocation(t *testing.T) {
    33  	f := newTCFixture(t)
    34  	t1 := time.Now()
    35  	f.clock.now = t1
    36  
    37  	f.setLastRun(t1)
    38  	f.run()
    39  
    40  	f.assertNoInvocation()
    41  }
    42  
    43  func TestTelScriptTimeIsNotUpNoInvocation(t *testing.T) {
    44  	f := newTCFixture(t)
    45  	t1 := time.Now()
    46  	f.clock.now = t1
    47  
    48  	f.workCmd()
    49  	f.setLastRun(t1)
    50  	f.run()
    51  
    52  	f.assertNoInvocation()
    53  }
    54  
    55  func TestTelScriptTimeIsUpNoSpansNoInvocation(t *testing.T) {
    56  	f := newTCFixture(t)
    57  	t1 := time.Now()
    58  	f.clock.now = t1
    59  
    60  	f.spans = nil
    61  	f.workCmd()
    62  	f.setLastRun(t1)
    63  	f.run()
    64  
    65  	f.assertNoInvocation()
    66  }
    67  
    68  func TestTelScriptTimeIsUpShouldClearSpansAndSetTime(t *testing.T) {
    69  	f := newTCFixture(t)
    70  	t1 := time.Now()
    71  	f.clock.now = t1
    72  
    73  	f.workCmd()
    74  	f.run()
    75  
    76  	f.assertInvocation()
    77  	f.assertCmdOutput(`{"SpanContext":{"TraceID":"00000000000000000000000000000000","SpanID":"0000000000000000","TraceFlags":0},"ParentSpanID":"0000000000000000","SpanKind":0,"Name":"","StartTime":"0001-01-01T00:00:00Z","EndTime":"0001-01-01T00:00:00Z","Attributes":null,"MessageEvents":null,"Links":null,"Status":0,"HasRemoteParent":false,"DroppedAttributeCount":0,"DroppedMessageEventCount":0,"DroppedLinkCount":0,"ChildSpanCount":0}
    78  `)
    79  	f.assertNoLogs()
    80  	f.assertTelemetryScriptRanAtIs(t1)
    81  	f.assertNoSpans()
    82  }
    83  
    84  func TestTelScriptFailsTimeIsUpShouldDeleteFileAndSetTime(t *testing.T) {
    85  	f := newTCFixture(t)
    86  	t1 := time.Now()
    87  	f.clock.now = t1
    88  
    89  	f.failCmd()
    90  	f.run()
    91  
    92  	f.assertInvocation()
    93  	f.assertLog("exit status 1")
    94  	f.assertSpansPresent()
    95  	f.assertTelemetryScriptRanAtIs(t1)
    96  }
    97  
    98  type tcFixture struct {
    99  	t          *testing.T
   100  	ctx        context.Context
   101  	temp       *tempdir.TempDirFixture
   102  	clock      fakeClock
   103  	st         *store.TestingStore
   104  	cmd        string
   105  	lastRun    time.Time
   106  	spans      []trace.ReadOnlySpan
   107  	sc         *tracer.SpanCollector
   108  	controller *Controller
   109  }
   110  
   111  func newTCFixture(t *testing.T) *tcFixture {
   112  	temp := tempdir.NewTempDirFixture(t)
   113  
   114  	st := store.NewTestingStore()
   115  
   116  	ctx := context.Background()
   117  
   118  	return &tcFixture{
   119  		t:     t,
   120  		ctx:   ctx,
   121  		temp:  temp,
   122  		clock: fakeClock{now: time.Unix(1551202573, 0)},
   123  		st:    st,
   124  		sc:    tracer.NewSpanCollector(ctx),
   125  		spans: []trace.ReadOnlySpan{tracetest.SpanStub{}.Snapshot()},
   126  	}
   127  }
   128  
   129  func (tcf *tcFixture) workCmd() {
   130  	ranTxt := tcf.temp.JoinPath("ran.txt")
   131  	out := tcf.temp.JoinPath("scriptstdout")
   132  
   133  	// A little python script that touches the ran.txt file
   134  	// and sends stdin to a file.
   135  	tcf.temp.WriteFile("work.py", fmt.Sprintf(`
   136  import sys
   137  
   138  open(%q, 'w').close()
   139  out = open(%q, 'w')
   140  out.write(sys.stdin.read())
   141  out.close()
   142  `, ranTxt, out))
   143  	if runtime.GOOS == "windows" {
   144  		tcf.cmd = fmt.Sprintf("python %s", tcf.temp.JoinPath("work.py"))
   145  		return
   146  	}
   147  	tcf.cmd = fmt.Sprintf("python3 %s", tcf.temp.JoinPath("work.py"))
   148  }
   149  
   150  func (tcf *tcFixture) failCmd() {
   151  	if runtime.GOOS == "windows" {
   152  		tcf.cmd = fmt.Sprintf("type nul > %s && exit 1", tcf.temp.JoinPath("ran.txt"))
   153  		return
   154  	}
   155  	tcf.cmd = fmt.Sprintf("touch %s; false", tcf.temp.JoinPath("ran.txt"))
   156  }
   157  
   158  func (tcf *tcFixture) assertNoInvocation() {
   159  	tcf.t.Helper()
   160  	_, err := os.Stat(tcf.temp.JoinPath("ran.txt"))
   161  	if !os.IsNotExist(err) {
   162  		tcf.t.Fatalf("expected ran.txt to not exist")
   163  	}
   164  }
   165  
   166  func (tcf *tcFixture) assertInvocation() {
   167  	tcf.t.Helper()
   168  	_, err := os.Stat(tcf.temp.JoinPath("ran.txt"))
   169  	if err != nil {
   170  		tcf.t.Fatalf("error stat'ing ran.txt: %v", err)
   171  	}
   172  }
   173  
   174  func (tcf *tcFixture) setLastRun(t time.Time) {
   175  	tcf.lastRun = t
   176  }
   177  
   178  func (tcf *tcFixture) run() {
   179  	tcf.t.Helper()
   180  	require.NoError(tcf.t, tcf.sc.ExportSpans(tcf.ctx, tcf.spans))
   181  
   182  	ts := model.TelemetrySettings{
   183  		Cmd:     model.ToHostCmd(tcf.cmd),
   184  		Workdir: tcf.temp.Path(),
   185  	}
   186  	tcf.st.SetState(store.EngineState{
   187  		TelemetrySettings: ts,
   188  	})
   189  
   190  	tc := NewController(tcf.clock, tcf.sc)
   191  	tc.lastRunAt = tcf.lastRun
   192  	tcf.controller = tc
   193  	_ = tc.OnChange(tcf.ctx, tcf.st, store.LegacyChangeSummary())
   194  }
   195  
   196  func (tcf *tcFixture) assertNoLogs() {
   197  	actions := tcf.st.Actions()
   198  	for _, a := range actions {
   199  		if la, ok := a.(store.LogAction); ok {
   200  			tcf.t.Errorf("Expected no LogActions but found: %v", la)
   201  		}
   202  	}
   203  }
   204  
   205  func (tcf *tcFixture) assertLog(logMsg string) {
   206  	actions := tcf.st.Actions()
   207  	for _, a := range actions {
   208  		if la, ok := a.(store.LogAction); ok {
   209  			containsExpected := strings.Contains(string(la.Message()), logMsg)
   210  			if containsExpected {
   211  				return
   212  			}
   213  		}
   214  	}
   215  
   216  	tcf.t.Errorf("Couldn't find expected log message %s in %v", logMsg, actions)
   217  }
   218  
   219  func (tcf *tcFixture) assertTelemetryScriptRanAtIs(t time.Time) {
   220  	assert.Equal(tcf.t, t, tcf.controller.lastRunAt)
   221  }
   222  
   223  func (tcf *tcFixture) assertCmdOutput(expected string) {
   224  	bs, err := os.ReadFile(tcf.temp.JoinPath("scriptstdout"))
   225  	if err != nil {
   226  		tcf.t.Fatal(err)
   227  	}
   228  
   229  	assert.Equal(tcf.t, normalize(expected), normalize(string(bs)))
   230  }
   231  
   232  func normalize(s string) string {
   233  	return strings.ReplaceAll(s, "\r\n", "\n")
   234  }
   235  
   236  func (tcf *tcFixture) assertSpansPresent() {
   237  	_, _, err := tcf.sc.GetOutgoingSpans()
   238  	if err != nil {
   239  		tcf.t.Fatalf("error getting spans: %v", err)
   240  	}
   241  }
   242  
   243  func (tcf *tcFixture) assertNoSpans() {
   244  	r, _, err := tcf.sc.GetOutgoingSpans()
   245  	if err != io.EOF {
   246  		tcf.t.Fatalf("Didn't get EOF for spans: %v %v", r, err)
   247  	}
   248  }
   249  
   250  type fakeClock struct {
   251  	now time.Time
   252  }
   253  
   254  func (c fakeClock) Now() time.Time { return c.now }