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  }