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 }