github.com/MetalBlockchain/metalgo@v1.11.9/snow/engine/snowman/job/scheduler_test.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package job
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  const (
    15  	depToResolve = iota
    16  	depToNeglect
    17  )
    18  
    19  var errDuplicateExecution = errors.New("job already executed")
    20  
    21  type testJob struct {
    22  	calledExecute bool
    23  	fulfilled     []int
    24  	abandoned     []int
    25  }
    26  
    27  func (j *testJob) Execute(_ context.Context, fulfilled []int, abandoned []int) error {
    28  	if j.calledExecute {
    29  		return errDuplicateExecution
    30  	}
    31  	j.calledExecute = true
    32  	j.fulfilled = fulfilled
    33  	j.abandoned = abandoned
    34  	return nil
    35  }
    36  
    37  func (j *testJob) reset() {
    38  	j.calledExecute = false
    39  	j.fulfilled = nil
    40  	j.abandoned = nil
    41  }
    42  
    43  func newSchedulerWithJob[T comparable](
    44  	t *testing.T,
    45  	job Job[T],
    46  	dependencies []T,
    47  	fulfilled []T,
    48  	abandoned []T,
    49  ) *Scheduler[T] {
    50  	s := NewScheduler[T]()
    51  	require.NoError(t, s.Schedule(context.Background(), job, dependencies...))
    52  	for _, d := range fulfilled {
    53  		require.NoError(t, s.Fulfill(context.Background(), d))
    54  	}
    55  	for _, d := range abandoned {
    56  		require.NoError(t, s.Abandon(context.Background(), d))
    57  	}
    58  	return s
    59  }
    60  
    61  func TestScheduler_Schedule(t *testing.T) {
    62  	userJob := &testJob{}
    63  	tests := []struct {
    64  		name                    string
    65  		scheduler               *Scheduler[int]
    66  		dependencies            []int
    67  		expectedExecuted        bool
    68  		expectedNumDependencies int
    69  		expectedScheduler       *Scheduler[int]
    70  	}{
    71  		{
    72  			name:                    "no dependencies",
    73  			scheduler:               NewScheduler[int](),
    74  			dependencies:            nil,
    75  			expectedExecuted:        true,
    76  			expectedNumDependencies: 0,
    77  			expectedScheduler:       NewScheduler[int](),
    78  		},
    79  		{
    80  			name:                    "one dependency",
    81  			scheduler:               NewScheduler[int](),
    82  			dependencies:            []int{depToResolve},
    83  			expectedExecuted:        false,
    84  			expectedNumDependencies: 1,
    85  			expectedScheduler: &Scheduler[int]{
    86  				dependents: map[int][]*job[int]{
    87  					depToResolve: {
    88  						{
    89  							numUnresolved: 1,
    90  							fulfilled:     nil,
    91  							abandoned:     nil,
    92  							job:           userJob,
    93  						},
    94  					},
    95  				},
    96  			},
    97  		},
    98  		{
    99  			name:                    "two dependencies",
   100  			scheduler:               NewScheduler[int](),
   101  			dependencies:            []int{depToResolve, depToNeglect},
   102  			expectedExecuted:        false,
   103  			expectedNumDependencies: 2,
   104  			expectedScheduler: &Scheduler[int]{
   105  				dependents: map[int][]*job[int]{
   106  					depToResolve: {
   107  						{
   108  							numUnresolved: 2,
   109  							fulfilled:     nil,
   110  							abandoned:     nil,
   111  							job:           userJob,
   112  						},
   113  					},
   114  					depToNeglect: {
   115  						{
   116  							numUnresolved: 2,
   117  							fulfilled:     nil,
   118  							abandoned:     nil,
   119  							job:           userJob,
   120  						},
   121  					},
   122  				},
   123  			},
   124  		},
   125  		{
   126  			name:                    "additional dependency",
   127  			scheduler:               newSchedulerWithJob(t, userJob, []int{depToResolve}, nil, nil),
   128  			dependencies:            []int{depToResolve},
   129  			expectedExecuted:        false,
   130  			expectedNumDependencies: 1,
   131  			expectedScheduler: &Scheduler[int]{
   132  				dependents: map[int][]*job[int]{
   133  					depToResolve: {
   134  						{
   135  							numUnresolved: 1,
   136  							fulfilled:     nil,
   137  							abandoned:     nil,
   138  							job:           userJob,
   139  						},
   140  						{
   141  							numUnresolved: 1,
   142  							fulfilled:     nil,
   143  							abandoned:     nil,
   144  							job:           userJob,
   145  						},
   146  					},
   147  				},
   148  			},
   149  		},
   150  	}
   151  	for _, test := range tests {
   152  		t.Run(test.name, func(t *testing.T) {
   153  			require := require.New(t)
   154  
   155  			// Reset the variable between tests
   156  			userJob.reset()
   157  
   158  			require.NoError(test.scheduler.Schedule(context.Background(), userJob, test.dependencies...))
   159  			require.Equal(test.expectedNumDependencies, test.scheduler.NumDependencies())
   160  			require.Equal(test.expectedExecuted, userJob.calledExecute)
   161  			require.Empty(userJob.fulfilled)
   162  			require.Empty(userJob.abandoned)
   163  			require.Equal(test.expectedScheduler, test.scheduler)
   164  		})
   165  	}
   166  }
   167  
   168  func TestScheduler_Fulfill(t *testing.T) {
   169  	userJob := &testJob{}
   170  	tests := []struct {
   171  		name              string
   172  		scheduler         *Scheduler[int]
   173  		expectedExecuted  bool
   174  		expectedFulfilled []int
   175  		expectedAbandoned []int
   176  		expectedScheduler *Scheduler[int]
   177  	}{
   178  		{
   179  			name:              "no jobs",
   180  			scheduler:         NewScheduler[int](),
   181  			expectedExecuted:  false,
   182  			expectedFulfilled: nil,
   183  			expectedAbandoned: nil,
   184  			expectedScheduler: NewScheduler[int](),
   185  		},
   186  		{
   187  			name:              "single dependency",
   188  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve}, nil, nil),
   189  			expectedExecuted:  true,
   190  			expectedFulfilled: []int{depToResolve},
   191  			expectedAbandoned: nil,
   192  			expectedScheduler: NewScheduler[int](),
   193  		},
   194  		{
   195  			name:              "non-existent dependency",
   196  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToNeglect}, nil, nil),
   197  			expectedExecuted:  false,
   198  			expectedFulfilled: nil,
   199  			expectedAbandoned: nil,
   200  			expectedScheduler: newSchedulerWithJob(t, userJob, []int{depToNeglect}, nil, nil),
   201  		},
   202  		{
   203  			name:              "incomplete dependencies",
   204  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve, depToNeglect}, nil, nil),
   205  			expectedExecuted:  false,
   206  			expectedFulfilled: nil,
   207  			expectedAbandoned: nil,
   208  			expectedScheduler: &Scheduler[int]{
   209  				dependents: map[int][]*job[int]{
   210  					depToNeglect: {
   211  						{
   212  							numUnresolved: 1,
   213  							fulfilled:     []int{depToResolve},
   214  							abandoned:     nil,
   215  							job:           userJob,
   216  						},
   217  					},
   218  				},
   219  			},
   220  		},
   221  		{
   222  			name:              "duplicate dependency",
   223  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve, depToResolve}, nil, nil),
   224  			expectedExecuted:  true,
   225  			expectedFulfilled: []int{depToResolve, depToResolve},
   226  			expectedAbandoned: nil,
   227  			expectedScheduler: NewScheduler[int](),
   228  		},
   229  		{
   230  			name:              "previously abandoned",
   231  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve, depToNeglect}, nil, []int{depToNeglect}),
   232  			expectedExecuted:  true,
   233  			expectedFulfilled: []int{depToResolve},
   234  			expectedAbandoned: []int{depToNeglect},
   235  			expectedScheduler: NewScheduler[int](),
   236  		},
   237  	}
   238  	for _, test := range tests {
   239  		t.Run(test.name, func(t *testing.T) {
   240  			require := require.New(t)
   241  
   242  			// Reset the variable between tests
   243  			userJob.reset()
   244  
   245  			require.NoError(test.scheduler.Fulfill(context.Background(), depToResolve))
   246  			require.Equal(test.expectedExecuted, userJob.calledExecute)
   247  			require.Equal(test.expectedFulfilled, userJob.fulfilled)
   248  			require.Equal(test.expectedAbandoned, userJob.abandoned)
   249  			require.Equal(test.expectedScheduler, test.scheduler)
   250  		})
   251  	}
   252  }
   253  
   254  func TestScheduler_Abandon(t *testing.T) {
   255  	userJob := &testJob{}
   256  	tests := []struct {
   257  		name              string
   258  		scheduler         *Scheduler[int]
   259  		expectedExecuted  bool
   260  		expectedFulfilled []int
   261  		expectedAbandoned []int
   262  		expectedScheduler *Scheduler[int]
   263  	}{
   264  		{
   265  			name:              "no jobs",
   266  			scheduler:         NewScheduler[int](),
   267  			expectedExecuted:  false,
   268  			expectedFulfilled: nil,
   269  			expectedAbandoned: nil,
   270  			expectedScheduler: NewScheduler[int](),
   271  		},
   272  		{
   273  			name:              "single dependency",
   274  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve}, nil, nil),
   275  			expectedExecuted:  true,
   276  			expectedFulfilled: nil,
   277  			expectedAbandoned: []int{depToResolve},
   278  			expectedScheduler: NewScheduler[int](),
   279  		},
   280  		{
   281  			name:              "non-existent dependency",
   282  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToNeglect}, nil, nil),
   283  			expectedExecuted:  false,
   284  			expectedFulfilled: nil,
   285  			expectedAbandoned: nil,
   286  			expectedScheduler: newSchedulerWithJob(t, userJob, []int{depToNeglect}, nil, nil),
   287  		},
   288  		{
   289  			name:              "incomplete dependencies",
   290  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve, depToNeglect}, nil, nil),
   291  			expectedExecuted:  false,
   292  			expectedFulfilled: nil,
   293  			expectedAbandoned: nil,
   294  			expectedScheduler: &Scheduler[int]{
   295  				dependents: map[int][]*job[int]{
   296  					depToNeglect: {
   297  						{
   298  							numUnresolved: 1,
   299  							fulfilled:     nil,
   300  							abandoned:     []int{depToResolve},
   301  							job:           userJob,
   302  						},
   303  					},
   304  				},
   305  			},
   306  		},
   307  		{
   308  			name:              "duplicate dependency",
   309  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve, depToResolve}, nil, nil),
   310  			expectedExecuted:  true,
   311  			expectedFulfilled: nil,
   312  			expectedAbandoned: []int{depToResolve, depToResolve},
   313  			expectedScheduler: NewScheduler[int](),
   314  		},
   315  		{
   316  			name:              "previously fulfilled",
   317  			scheduler:         newSchedulerWithJob(t, userJob, []int{depToResolve, depToNeglect}, []int{depToNeglect}, nil),
   318  			expectedExecuted:  true,
   319  			expectedFulfilled: []int{depToNeglect},
   320  			expectedAbandoned: []int{depToResolve},
   321  			expectedScheduler: NewScheduler[int](),
   322  		},
   323  	}
   324  	for _, test := range tests {
   325  		t.Run(test.name, func(t *testing.T) {
   326  			require := require.New(t)
   327  
   328  			// Reset the variable between tests
   329  			userJob.reset()
   330  
   331  			require.NoError(test.scheduler.Abandon(context.Background(), depToResolve))
   332  			require.Equal(test.expectedExecuted, userJob.calledExecute)
   333  			require.Equal(test.expectedFulfilled, userJob.fulfilled)
   334  			require.Equal(test.expectedAbandoned, userJob.abandoned)
   335  			require.Equal(test.expectedScheduler, test.scheduler)
   336  		})
   337  	}
   338  }