golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/schedule_test.go (about)

     1  // Copyright 2022 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  	"testing"
    11  	"time"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/google/go-cmp/cmp/cmpopts"
    15  	"github.com/robfig/cron/v3"
    16  	"golang.org/x/build/internal/relui/db"
    17  )
    18  
    19  func mustParseSpec(t *testing.T, spec string) cron.Schedule {
    20  	t.Helper()
    21  	sched, err := cron.ParseStandard(spec)
    22  	if err != nil {
    23  		t.Fatalf("cron.ParseStandard(%q) = %q, wanted no error", spec, err)
    24  	}
    25  	return sched
    26  }
    27  
    28  func TestSchedulerCreate(t *testing.T) {
    29  	now := time.Now()
    30  	cases := []struct {
    31  		desc         string
    32  		sched        Schedule
    33  		workflowName string
    34  		params       map[string]any
    35  		want         db.Schedule
    36  		wantEntries  []ScheduleEntry
    37  		wantErr      bool
    38  	}{
    39  		{
    40  			desc:         "success: once",
    41  			sched:        Schedule{Once: now.AddDate(1, 0, 0), Type: ScheduleOnce},
    42  			workflowName: "echo",
    43  			params:       map[string]any{"greeting": "hello", "farewell": "bye"},
    44  			want: db.Schedule{
    45  				WorkflowName: "echo",
    46  				WorkflowParams: sql.NullString{
    47  					String: `{"farewell": "bye", "greeting": "hello"}`,
    48  					Valid:  true,
    49  				},
    50  				Once:      now.AddDate(1, 0, 0),
    51  				CreatedAt: now,
    52  				UpdatedAt: now,
    53  			},
    54  			wantEntries: []ScheduleEntry{
    55  				{Entry: cron.Entry{
    56  					Schedule: &RunOnce{next: now.UTC().AddDate(1, 0, 0)},
    57  					Next:     now.UTC().AddDate(1, 0, 0),
    58  					Job: &WorkflowSchedule{
    59  						Schedule: db.Schedule{
    60  							WorkflowName: "echo",
    61  							WorkflowParams: sql.NullString{
    62  								String: `{"farewell": "bye", "greeting": "hello"}`,
    63  								Valid:  true,
    64  							},
    65  							Once:      now.UTC().AddDate(1, 0, 0),
    66  							CreatedAt: now,
    67  							UpdatedAt: now,
    68  						},
    69  						Params: map[string]any{"greeting": "hello", "farewell": "bye"},
    70  					},
    71  				}},
    72  			},
    73  		},
    74  		{
    75  			desc:         "success: cron",
    76  			sched:        Schedule{Cron: "* * * * *", Type: ScheduleCron},
    77  			workflowName: "echo",
    78  			params:       map[string]any{"greeting": "hello", "farewell": "bye"},
    79  			want: db.Schedule{
    80  				WorkflowName: "echo",
    81  				WorkflowParams: sql.NullString{
    82  					String: `{"farewell": "bye", "greeting": "hello"}`,
    83  					Valid:  true,
    84  				},
    85  				Spec:      "* * * * *",
    86  				CreatedAt: now,
    87  				UpdatedAt: now,
    88  			},
    89  			wantEntries: []ScheduleEntry{
    90  				{Entry: cron.Entry{
    91  					Schedule: mustParseSpec(t, "* * * * *"),
    92  					Next:     now.Add(time.Minute),
    93  					Job: &WorkflowSchedule{
    94  						Schedule: db.Schedule{
    95  							WorkflowName: "echo",
    96  							WorkflowParams: sql.NullString{
    97  								String: `{"farewell": "bye", "greeting": "hello"}`,
    98  								Valid:  true,
    99  							},
   100  							Spec:      "* * * * *",
   101  							CreatedAt: now,
   102  							UpdatedAt: now,
   103  						},
   104  						Params: map[string]any{"greeting": "hello", "farewell": "bye"},
   105  					},
   106  				}},
   107  			},
   108  		},
   109  		{
   110  			desc:         "error: invalid Schedule",
   111  			sched:        Schedule{Type: ScheduleImmediate},
   112  			workflowName: "echo",
   113  			params:       map[string]any{"greeting": "hello", "farewell": "bye"},
   114  			wantErr:      true,
   115  			wantEntries:  []ScheduleEntry{},
   116  		},
   117  	}
   118  	for _, c := range cases {
   119  		t.Run(c.desc, func(t *testing.T) {
   120  			ctx, cancel := context.WithCancel(context.Background())
   121  			defer cancel()
   122  			p := testDB(ctx, t)
   123  			s := NewScheduler(p, NewWorker(NewDefinitionHolder(), p, &PGListener{DB: p}))
   124  			row, err := s.Create(ctx, c.sched, c.workflowName, c.params)
   125  			if (err != nil) != c.wantErr {
   126  				t.Fatalf("s.Create(_, %v, %q, %v) = %v, %v, wantErr: %t", c.sched, c.workflowName, c.params, row, err, c.wantErr)
   127  			}
   128  			if diff := cmp.Diff(c.want, row, cmpopts.EquateApproxTime(time.Minute), cmpopts.IgnoreFields(db.Schedule{}, "ID")); diff != "" {
   129  				t.Fatalf("s.Create() mismatch (-want +got):\n%s", diff)
   130  			}
   131  			got := s.Entries()
   132  
   133  			diffOpts := []cmp.Option{
   134  				cmpopts.EquateApproxTime(time.Minute),
   135  				cmpopts.IgnoreFields(db.Schedule{}, "ID"),
   136  				cmpopts.IgnoreUnexported(RunOnce{}, WorkflowSchedule{}, time.Location{}),
   137  				cmpopts.IgnoreFields(ScheduleEntry{}, "ID", "LastRun.ID", "WrappedJob"),
   138  			}
   139  			if diff := cmp.Diff(c.wantEntries, got, diffOpts...); diff != "" {
   140  				t.Fatalf("s.Entries() mismatch (-want +got):\n%s", diff)
   141  			}
   142  		})
   143  	}
   144  }
   145  
   146  func TestSchedulerResume(t *testing.T) {
   147  	now := time.Now()
   148  	cases := []struct {
   149  		desc       string
   150  		scheds     []db.CreateScheduleParams
   151  		want       []ScheduleEntry
   152  		wantParams map[string]any
   153  		wantErr    bool
   154  	}{
   155  		{
   156  			desc: "success: once",
   157  			scheds: []db.CreateScheduleParams{
   158  				{
   159  					WorkflowName: "echo",
   160  					WorkflowParams: sql.NullString{
   161  						String: `{"farewell": "bye", "greeting": "hello"}`,
   162  						Valid:  true,
   163  					},
   164  					Once:      now.UTC().AddDate(1, 0, 0),
   165  					CreatedAt: now,
   166  					UpdatedAt: now,
   167  				},
   168  			},
   169  			want: []ScheduleEntry{
   170  				{Entry: cron.Entry{
   171  					Schedule: &RunOnce{next: now.UTC().AddDate(1, 0, 0)},
   172  					Next:     now.UTC().AddDate(1, 0, 0),
   173  					Job: &WorkflowSchedule{
   174  						Schedule: db.Schedule{
   175  							WorkflowName: "echo",
   176  							WorkflowParams: sql.NullString{
   177  								String: `{"farewell": "bye", "greeting": "hello"}`,
   178  								Valid:  true,
   179  							},
   180  							Once:      now.UTC().AddDate(1, 0, 0),
   181  							CreatedAt: now,
   182  							UpdatedAt: now,
   183  						},
   184  						Params: map[string]any{"greeting": "hello", "farewell": "bye"},
   185  					},
   186  				}},
   187  			},
   188  		},
   189  		{
   190  			desc: "success: cron",
   191  			scheds: []db.CreateScheduleParams{
   192  				{
   193  					WorkflowName: "echo",
   194  					WorkflowParams: sql.NullString{
   195  						String: `{"farewell": "bye", "greeting": "hello"}`,
   196  						Valid:  true,
   197  					},
   198  					Spec:      "* * * * *",
   199  					CreatedAt: now,
   200  					UpdatedAt: now,
   201  				},
   202  			},
   203  			want: []ScheduleEntry{
   204  				{Entry: cron.Entry{
   205  					Schedule: mustParseSpec(t, "* * * * *"),
   206  					Next:     now.Add(time.Minute),
   207  					Job: &WorkflowSchedule{
   208  						Schedule: db.Schedule{
   209  							WorkflowName: "echo",
   210  							WorkflowParams: sql.NullString{
   211  								String: `{"farewell": "bye", "greeting": "hello"}`,
   212  								Valid:  true,
   213  							},
   214  							Spec:      "* * * * *",
   215  							CreatedAt: now,
   216  							UpdatedAt: now,
   217  						},
   218  						Params: map[string]any{"greeting": "hello", "farewell": "bye"},
   219  					},
   220  				}},
   221  			},
   222  		},
   223  		{
   224  			desc: "skip past RunOnce schedules",
   225  			scheds: []db.CreateScheduleParams{
   226  				{
   227  					WorkflowName: "echo",
   228  					WorkflowParams: sql.NullString{
   229  						String: `{"farewell": "bye", "greeting": "hello"}`,
   230  						Valid:  true,
   231  					},
   232  					Once:      time.Now().AddDate(-1, 0, 0),
   233  					CreatedAt: now,
   234  					UpdatedAt: now,
   235  				},
   236  			},
   237  			want: []ScheduleEntry{},
   238  		},
   239  	}
   240  	for _, c := range cases {
   241  		t.Run(c.desc, func(t *testing.T) {
   242  			ctx, cancel := context.WithCancel(context.Background())
   243  			defer cancel()
   244  			p := testDB(ctx, t)
   245  			q := db.New(p)
   246  			s := NewScheduler(p, NewWorker(NewDefinitionHolder(), p, &PGListener{DB: p}))
   247  
   248  			for _, csp := range c.scheds {
   249  				if _, err := q.CreateSchedule(ctx, csp); err != nil {
   250  					t.Fatalf("q.CreateSchedule(_, %#v) = _, %v, wanted no error", csp, err)
   251  				}
   252  			}
   253  			if err := s.Resume(ctx); err != nil {
   254  				t.Errorf("s.Resume() = %v, wanted no error", err)
   255  			}
   256  			got := s.Entries()
   257  
   258  			diffOpts := []cmp.Option{
   259  				cmpopts.EquateApproxTime(time.Minute),
   260  				cmpopts.IgnoreFields(db.Schedule{}, "ID"),
   261  				cmpopts.IgnoreUnexported(RunOnce{}, WorkflowSchedule{}, time.Location{}),
   262  				cmpopts.IgnoreFields(ScheduleEntry{}, "ID", "LastRun.ID", "WrappedJob"),
   263  			}
   264  			if diff := cmp.Diff(c.want, got, diffOpts...); diff != "" {
   265  				t.Fatalf("s.Entries() mismatch (-want +got):\n%s", diff)
   266  			}
   267  		})
   268  	}
   269  }
   270  
   271  func TestScheduleDelete(t *testing.T) {
   272  	now := time.Now()
   273  	cases := []struct {
   274  		desc          string
   275  		sched         Schedule
   276  		workflowName  string
   277  		params        map[string]any
   278  		wantErr       bool
   279  		wantEntries   []ScheduleEntry
   280  		want          []db.Schedule
   281  		wantWorkflows []db.Workflow
   282  	}{
   283  		{
   284  			desc:         "success",
   285  			sched:        Schedule{Once: now.AddDate(1, 0, 0), Type: ScheduleOnce},
   286  			workflowName: "echo",
   287  			params:       map[string]any{"greeting": "hello", "farewell": "bye"},
   288  			wantEntries:  []ScheduleEntry{},
   289  		},
   290  	}
   291  	for _, c := range cases {
   292  		t.Run(c.desc, func(t *testing.T) {
   293  			ctx, cancel := context.WithCancel(context.Background())
   294  			defer cancel()
   295  			p := testDB(ctx, t)
   296  			q := db.New(p)
   297  			s := NewScheduler(p, NewWorker(NewDefinitionHolder(), p, &PGListener{DB: p}))
   298  			row, err := s.Create(ctx, c.sched, c.workflowName, c.params)
   299  			if err != nil {
   300  				t.Fatalf("s.Create(_, %v, %q, %v) = %v, %v, wanted no error", c.sched, c.workflowName, c.params, row, err)
   301  			}
   302  			// simulate a single run
   303  			wfid, err := s.w.StartWorkflow(ctx, c.workflowName, c.params, int(row.ID))
   304  			if err != nil {
   305  				t.Fatalf("s.w.StartWorkflow(_, %q, %v, %d) = %q, %v, wanted no error", c.workflowName, c.params, row.ID, wfid.String(), err)
   306  			}
   307  
   308  			err = s.Delete(ctx, int(row.ID))
   309  			if (err != nil) != c.wantErr {
   310  				t.Fatalf("s.Delete(%d) = %v, wantErr: %t", row.ID, err, c.wantErr)
   311  			}
   312  
   313  			entries := s.Entries()
   314  			diffOpts := []cmp.Option{
   315  				cmpopts.EquateApproxTime(time.Minute),
   316  				cmpopts.IgnoreFields(db.Schedule{}, "ID"),
   317  				cmpopts.IgnoreUnexported(RunOnce{}, WorkflowSchedule{}, time.Location{}),
   318  				cmpopts.IgnoreFields(ScheduleEntry{}, "ID", "LastRun.ID", "WrappedJob"),
   319  			}
   320  			if c.sched.Type == ScheduleCron {
   321  				diffOpts = append(diffOpts, cmpopts.IgnoreFields(ScheduleEntry{}, "Next"))
   322  			}
   323  			if diff := cmp.Diff(c.wantEntries, entries, diffOpts...); diff != "" {
   324  				t.Errorf("s.Entries() mismatch (-want +got):\n%s", diff)
   325  			}
   326  			got, err := q.Schedules(ctx)
   327  			if err != nil {
   328  				t.Fatalf("q.Schedules() = %v, %v, wanted no error", got, err)
   329  			}
   330  			if diff := cmp.Diff(c.want, got, diffOpts...); diff != "" {
   331  				t.Errorf("q.Schedules() mismatch (-want +got):\n%s", diff)
   332  			}
   333  			wfs, err := q.Workflows(ctx)
   334  			if err != nil {
   335  				t.Fatalf("q.Workflows() = %v, %v, wanted no error", wfs, err)
   336  			}
   337  			if len(wfs) != 1 {
   338  				t.Errorf("len(q.Workflows()) = %d, wanted %d", len(wfs), 1)
   339  			}
   340  			for _, w := range wfs {
   341  				if w.ScheduleID.Int32 == row.ID {
   342  					t.Errorf("w.ScheduleID = %d, wanted != %d", w.ScheduleID.Int32, row.ID)
   343  				}
   344  			}
   345  		})
   346  	}
   347  }