golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/listener_test.go (about) 1 // Copyright 2021 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package relui 6 7 import ( 8 "context" 9 "database/sql" 10 "errors" 11 "fmt" 12 "net/mail" 13 "testing" 14 "time" 15 16 "github.com/google/go-cmp/cmp" 17 "github.com/google/go-cmp/cmp/cmpopts" 18 "github.com/google/uuid" 19 "golang.org/x/build/internal/relui/db" 20 "golang.org/x/build/internal/task" 21 "golang.org/x/build/internal/workflow" 22 ) 23 24 func TestListenerTaskStateChanged(t *testing.T) { 25 ctx, cancel := context.WithCancel(context.Background()) 26 defer cancel() 27 dbp := testDB(ctx, t) 28 q := db.New(dbp) 29 30 cases := []struct { 31 desc string 32 state *workflow.TaskState 33 want []db.Task 34 }{ 35 { 36 desc: "records successful tasks", 37 state: &workflow.TaskState{ 38 Name: "TestTask", 39 Finished: true, 40 Result: struct{ Value int }{5}, 41 SerializedResult: []byte(`{"Value": 5}`), 42 Error: "", 43 }, 44 want: []db.Task{ 45 { 46 Name: "TestTask", 47 Finished: true, 48 Result: sql.NullString{String: `{"Value": 5}`, Valid: true}, 49 CreatedAt: time.Now(), // cmpopts.EquateApproxTime 50 UpdatedAt: time.Now(), // cmpopts.EquateApproxTime 51 }, 52 }, 53 }, 54 { 55 desc: "records failing tasks", 56 state: &workflow.TaskState{ 57 Name: "TestTask", 58 Finished: true, 59 Result: struct{ Value int }{5}, 60 SerializedResult: []byte(`{"Value": 5}`), 61 Error: "it's completely broken and hopeless", 62 }, 63 want: []db.Task{ 64 { 65 Name: "TestTask", 66 Finished: true, 67 Result: sql.NullString{String: `{"Value": 5}`, Valid: true}, 68 Error: sql.NullString{String: "it's completely broken and hopeless", Valid: true}, 69 CreatedAt: time.Now(), // cmpopts.EquateApproxTime 70 UpdatedAt: time.Now(), // cmpopts.EquateApproxTime 71 }, 72 }, 73 }, 74 } 75 for _, c := range cases { 76 t.Run(c.desc, func(t *testing.T) { 77 wfp := db.CreateWorkflowParams{ID: uuid.New()} 78 wf, err := q.CreateWorkflow(ctx, wfp) 79 if err != nil { 80 t.Fatalf("q.CreateWorkflow(%v, %v) = %v, wanted no error", ctx, wfp, err) 81 } 82 83 l := &PGListener{DB: dbp} 84 err = l.TaskStateChanged(wf.ID, "TestTask", c.state) 85 if err != nil { 86 t.Fatalf("l.TaskStateChanged(%v, %q, %v) = %v, wanted no error", wf.ID, "TestTask", c.state, err) 87 } 88 89 tasks, err := q.TasksForWorkflow(ctx, wf.ID) 90 if err != nil { 91 t.Fatalf("q.TasksForWorkflow(%v, %v) = %v, %v, wanted no error", ctx, wf.ID, tasks, err) 92 } 93 if diff := cmp.Diff(c.want, tasks, cmpopts.EquateApproxTime(time.Minute), cmpopts.IgnoreFields(db.Task{}, "WorkflowID")); diff != "" { 94 t.Errorf("q.TasksForWorkflow(_, %q) mismatch (-want +got):\n%s", wf.ID, diff) 95 } 96 }) 97 } 98 } 99 100 func TestListenerLogger(t *testing.T) { 101 ctx, cancel := context.WithCancel(context.Background()) 102 defer cancel() 103 dbp := testDB(ctx, t) 104 q := db.New(dbp) 105 106 wfp := db.CreateWorkflowParams{ID: uuid.New()} 107 wf, err := q.CreateWorkflow(ctx, wfp) 108 if err != nil { 109 t.Fatalf("q.CreateWorkflow(%v, %v) = %v, wanted no error", ctx, wfp, err) 110 } 111 params := db.UpsertTaskParams{WorkflowID: wf.ID, Name: "TestTask"} 112 _, err = q.UpsertTask(ctx, params) 113 if err != nil { 114 t.Fatalf("q.UpsertTask(%v, %v) = %v, wanted no error", ctx, params, err) 115 } 116 117 l := &PGListener{DB: dbp} 118 l.Logger(wf.ID, "TestTask").Printf("A fancy log line says %q", "hello") 119 120 logs, err := q.TaskLogs(ctx) 121 if err != nil { 122 t.Fatalf("q.TaskLogs(%v) = %v, wanted no error", ctx, err) 123 } 124 want := []db.TaskLog{{ 125 WorkflowID: wf.ID, 126 TaskName: "TestTask", 127 Body: `A fancy log line says "hello"`, 128 CreatedAt: time.Now(), // cmpopts.EquateApproxTime 129 UpdatedAt: time.Now(), // cmpopts.EquateApproxTime 130 }} 131 if diff := cmp.Diff(want, logs, cmpopts.EquateApproxTime(time.Minute), cmpopts.IgnoreFields(db.TaskLog{}, "ID")); diff != "" { 132 t.Errorf("q.TaskLogs(_, %q) mismatch (-want +got):\n%s", wf.ID, diff) 133 } 134 } 135 136 func TestPGListenerWorkflowStalledNotification(t *testing.T) { 137 cases := []struct { 138 desc string 139 schedule bool 140 taskErr bool 141 }{ 142 { 143 desc: "scheduled workflow failure sends notification", 144 schedule: true, 145 taskErr: true, 146 }, 147 { 148 desc: "scheduled workflow success sends nothing", 149 schedule: true, 150 }, 151 { 152 desc: "unscheduled workflow success sends nothing", 153 }, 154 { 155 desc: "unscheduled workflow failure sends nothing", 156 taskErr: true, 157 }, 158 } 159 for _, c := range cases { 160 t.Run(c.desc, func(t *testing.T) { 161 ctx, cancel := context.WithCancel(context.Background()) 162 defer cancel() 163 p := testDB(ctx, t) 164 var schedID int 165 if c.schedule { 166 sched, err := db.New(p).CreateSchedule(ctx, db.CreateScheduleParams{WorkflowName: c.desc}) 167 if err != nil { 168 t.Fatalf("CreateSchedule() = %v, wanted no error", err) 169 } 170 schedID = int(sched.ID) 171 } 172 wd := workflow.New() 173 complete := workflow.Task0(wd, "complete", func(ctx context.Context) (string, error) { 174 if c.taskErr { 175 return "", fmt.Errorf("c.taskErr: %t", c.taskErr) 176 } 177 return "done", nil 178 }) 179 workflow.Output(wd, "finished", complete) 180 dh := NewDefinitionHolder() 181 dh.RegisterDefinition(c.desc, wd) 182 183 header := task.MailHeader{ 184 From: mail.Address{Address: "from-address@golang.test"}, 185 To: mail.Address{Address: "to-address@golang.test"}, 186 } 187 var gotHeader task.MailHeader 188 var gotContent task.MailContent 189 pgl := &PGListener{ 190 DB: p, 191 SendMail: func(h task.MailHeader, c task.MailContent) error { 192 gotHeader, gotContent = h, c 193 return nil 194 }, 195 ScheduleFailureMailHeader: header, 196 } 197 listener := &testWorkflowListener{ 198 Listener: pgl, 199 onFinished: cancel, 200 } 201 w := NewWorker(dh, p, listener) 202 203 id, err := w.StartWorkflow(ctx, c.desc, nil, schedID) 204 if err != nil { 205 t.Fatalf("w.StartWorkflow(_, %q, %v, %d) = %v, %v, wanted no error", c.desc, nil, schedID, id, err) 206 } 207 listener.onStalled = func() { 208 w.cancelWorkflow(id) 209 } 210 if err := w.Run(ctx); !errors.Is(err, context.Canceled) { 211 t.Errorf("w.Run() = %v, wanted %v", err, context.Canceled) 212 } 213 214 wantSend := c.taskErr && c.schedule 215 if (gotContent.Subject == "") == wantSend { 216 t.Errorf("gotContent.Subject = %q, wanted empty: %t", gotContent.Subject, !c.taskErr) 217 } 218 if (gotContent.BodyText == "") == wantSend { 219 t.Errorf("gotContent.BodyText = %q, wanted empty: %t", gotContent.BodyText, !c.taskErr) 220 } 221 var wantHeader task.MailHeader 222 if wantSend { 223 wantHeader = header 224 } 225 if diff := cmp.Diff(wantHeader, gotHeader); diff != "" { 226 t.Errorf("WorkflowFinished(_, %q) mismatch (-want +got):\n%s", id, diff) 227 } 228 }) 229 } 230 }