github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/rc/jobs/job_test.go (about) 1 package jobs 2 3 import ( 4 "context" 5 "errors" 6 "runtime" 7 "testing" 8 "time" 9 10 "github.com/rclone/rclone/fs" 11 "github.com/rclone/rclone/fs/accounting" 12 "github.com/rclone/rclone/fs/filter" 13 "github.com/rclone/rclone/fs/rc" 14 "github.com/rclone/rclone/fs/rc/rcflags" 15 "github.com/rclone/rclone/fstest/testy" 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 ) 19 20 func TestNewJobs(t *testing.T) { 21 jobs := newJobs() 22 assert.Equal(t, 0, len(jobs.jobs)) 23 } 24 25 func TestJobsKickExpire(t *testing.T) { 26 testy.SkipUnreliable(t) 27 jobs := newJobs() 28 jobs.opt.JobExpireInterval = time.Millisecond 29 assert.Equal(t, false, jobs.expireRunning) 30 jobs.kickExpire() 31 jobs.mu.Lock() 32 assert.Equal(t, true, jobs.expireRunning) 33 jobs.mu.Unlock() 34 time.Sleep(10 * time.Millisecond) 35 jobs.mu.Lock() 36 assert.Equal(t, false, jobs.expireRunning) 37 jobs.mu.Unlock() 38 } 39 40 func TestJobsExpire(t *testing.T) { 41 testy.SkipUnreliable(t) 42 ctx := context.Background() 43 wait := make(chan struct{}) 44 jobs := newJobs() 45 jobs.opt.JobExpireInterval = time.Millisecond 46 assert.Equal(t, false, jobs.expireRunning) 47 var gotJobID int64 48 var gotJob *Job 49 job, out, err := jobs.NewJob(ctx, func(ctx context.Context, in rc.Params) (rc.Params, error) { 50 defer close(wait) 51 var ok bool 52 gotJobID, ok = GetJobID(ctx) 53 assert.True(t, ok) 54 gotJob, ok = GetJob(ctx) 55 assert.True(t, ok) 56 return in, nil 57 }, rc.Params{"_async": true}) 58 require.NoError(t, err) 59 assert.Equal(t, 1, len(out)) 60 <-wait 61 assert.Equal(t, job.ID, gotJobID, "check can get JobID from ctx") 62 assert.Equal(t, job, gotJob, "check can get Job from ctx") 63 assert.Equal(t, 1, len(jobs.jobs)) 64 jobs.Expire() 65 assert.Equal(t, 1, len(jobs.jobs)) 66 jobs.mu.Lock() 67 job.mu.Lock() 68 job.EndTime = time.Now().Add(-rcflags.Opt.JobExpireDuration - 60*time.Second) 69 assert.Equal(t, true, jobs.expireRunning) 70 job.mu.Unlock() 71 jobs.mu.Unlock() 72 time.Sleep(250 * time.Millisecond) 73 jobs.mu.Lock() 74 assert.Equal(t, false, jobs.expireRunning) 75 assert.Equal(t, 0, len(jobs.jobs)) 76 jobs.mu.Unlock() 77 } 78 79 var noopFn = func(ctx context.Context, in rc.Params) (rc.Params, error) { 80 return nil, nil 81 } 82 83 func TestJobsIDs(t *testing.T) { 84 ctx := context.Background() 85 jobs := newJobs() 86 job1, _, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true}) 87 require.NoError(t, err) 88 job2, _, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true}) 89 require.NoError(t, err) 90 wantIDs := []int64{job1.ID, job2.ID} 91 gotIDs := jobs.IDs() 92 require.Equal(t, 2, len(gotIDs)) 93 if gotIDs[0] != wantIDs[0] { 94 gotIDs[0], gotIDs[1] = gotIDs[1], gotIDs[0] 95 } 96 assert.Equal(t, wantIDs, gotIDs) 97 } 98 99 func TestJobsGet(t *testing.T) { 100 ctx := context.Background() 101 jobs := newJobs() 102 job, _, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true}) 103 require.NoError(t, err) 104 assert.Equal(t, job, jobs.Get(job.ID)) 105 assert.Nil(t, jobs.Get(123123123123)) 106 } 107 108 var longFn = func(ctx context.Context, in rc.Params) (rc.Params, error) { 109 time.Sleep(1 * time.Hour) 110 return nil, nil 111 } 112 113 var shortFn = func(ctx context.Context, in rc.Params) (rc.Params, error) { 114 time.Sleep(time.Millisecond) 115 return nil, nil 116 } 117 118 var ctxFn = func(ctx context.Context, in rc.Params) (rc.Params, error) { 119 <-ctx.Done() 120 return nil, ctx.Err() 121 } 122 123 var ctxParmFn = func(paramCtx context.Context, returnError bool) func(ctx context.Context, in rc.Params) (rc.Params, error) { 124 return func(ctx context.Context, in rc.Params) (rc.Params, error) { 125 <-paramCtx.Done() 126 if returnError { 127 return nil, ctx.Err() 128 } 129 return rc.Params{}, nil 130 } 131 } 132 133 const ( 134 sleepTime = 100 * time.Millisecond 135 floatSleepTime = float64(sleepTime) / 1e9 / 2 136 ) 137 138 // sleep for some time so job.Duration is non-0 139 func sleepJob() { 140 time.Sleep(sleepTime) 141 } 142 143 func TestJobFinish(t *testing.T) { 144 ctx := context.Background() 145 jobs := newJobs() 146 job, _, err := jobs.NewJob(ctx, longFn, rc.Params{"_async": true}) 147 require.NoError(t, err) 148 sleepJob() 149 150 assert.Equal(t, true, job.EndTime.IsZero()) 151 assert.Equal(t, rc.Params(nil), job.Output) 152 assert.Equal(t, 0.0, job.Duration) 153 assert.Equal(t, "", job.Error) 154 assert.Equal(t, false, job.Success) 155 assert.Equal(t, false, job.Finished) 156 157 wantOut := rc.Params{"a": 1} 158 job.finish(wantOut, nil) 159 160 assert.Equal(t, false, job.EndTime.IsZero()) 161 assert.Equal(t, wantOut, job.Output) 162 assert.True(t, job.Duration >= floatSleepTime) 163 assert.Equal(t, "", job.Error) 164 assert.Equal(t, true, job.Success) 165 assert.Equal(t, true, job.Finished) 166 167 job, _, err = jobs.NewJob(ctx, longFn, rc.Params{"_async": true}) 168 require.NoError(t, err) 169 sleepJob() 170 job.finish(nil, nil) 171 172 assert.Equal(t, false, job.EndTime.IsZero()) 173 assert.Equal(t, rc.Params{}, job.Output) 174 assert.True(t, job.Duration >= floatSleepTime) 175 assert.Equal(t, "", job.Error) 176 assert.Equal(t, true, job.Success) 177 assert.Equal(t, true, job.Finished) 178 179 job, _, err = jobs.NewJob(ctx, longFn, rc.Params{"_async": true}) 180 require.NoError(t, err) 181 sleepJob() 182 job.finish(wantOut, errors.New("potato")) 183 184 assert.Equal(t, false, job.EndTime.IsZero()) 185 assert.Equal(t, wantOut, job.Output) 186 assert.True(t, job.Duration >= floatSleepTime) 187 assert.Equal(t, "potato", job.Error) 188 assert.Equal(t, false, job.Success) 189 assert.Equal(t, true, job.Finished) 190 } 191 192 // We've tested the functionality of run() already as it is 193 // part of NewJob, now just test the panic catching 194 func TestJobRunPanic(t *testing.T) { 195 ctx := context.Background() 196 wait := make(chan struct{}) 197 boom := func(ctx context.Context, in rc.Params) (rc.Params, error) { 198 sleepJob() 199 defer close(wait) 200 panic("boom") 201 } 202 203 jobs := newJobs() 204 job, _, err := jobs.NewJob(ctx, boom, rc.Params{"_async": true}) 205 require.NoError(t, err) 206 <-wait 207 runtime.Gosched() // yield to make sure job is updated 208 209 // Wait a short time for the panic to propagate 210 for i := uint(0); i < 10; i++ { 211 job.mu.Lock() 212 e := job.Error 213 job.mu.Unlock() 214 if e != "" { 215 break 216 } 217 time.Sleep(time.Millisecond << i) 218 } 219 220 job.mu.Lock() 221 assert.Equal(t, false, job.EndTime.IsZero()) 222 assert.Equal(t, rc.Params{}, job.Output) 223 assert.True(t, job.Duration >= floatSleepTime) 224 assert.Contains(t, job.Error, "panic received: boom") 225 assert.Equal(t, false, job.Success) 226 assert.Equal(t, true, job.Finished) 227 job.mu.Unlock() 228 } 229 230 func TestJobsNewJob(t *testing.T) { 231 ctx := context.Background() 232 jobID.Store(0) 233 jobs := newJobs() 234 job, out, err := jobs.NewJob(ctx, noopFn, rc.Params{"_async": true}) 235 require.NoError(t, err) 236 assert.Equal(t, int64(1), job.ID) 237 assert.Equal(t, rc.Params{"jobid": int64(1)}, out) 238 assert.Equal(t, job, jobs.Get(1)) 239 assert.NotEmpty(t, job.Stop) 240 } 241 242 func TestStartJob(t *testing.T) { 243 ctx := context.Background() 244 jobID.Store(0) 245 job, out, err := NewJob(ctx, longFn, rc.Params{"_async": true}) 246 assert.NoError(t, err) 247 assert.Equal(t, rc.Params{"jobid": int64(1)}, out) 248 assert.Equal(t, int64(1), job.ID) 249 } 250 251 func TestExecuteJob(t *testing.T) { 252 jobID.Store(0) 253 job, out, err := NewJob(context.Background(), shortFn, rc.Params{}) 254 assert.NoError(t, err) 255 assert.Equal(t, int64(1), job.ID) 256 assert.Equal(t, rc.Params{}, out) 257 } 258 259 func TestExecuteJobWithConfig(t *testing.T) { 260 ctx := context.Background() 261 jobID.Store(0) 262 called := false 263 jobFn := func(ctx context.Context, in rc.Params) (rc.Params, error) { 264 ci := fs.GetConfig(ctx) 265 assert.Equal(t, 42*fs.Mebi, ci.BufferSize) 266 called = true 267 return nil, nil 268 } 269 _, _, err := NewJob(context.Background(), jobFn, rc.Params{ 270 "_config": rc.Params{ 271 "BufferSize": "42M", 272 }, 273 }) 274 require.NoError(t, err) 275 assert.Equal(t, true, called) 276 // Retest with string parameter 277 jobID.Store(0) 278 called = false 279 _, _, err = NewJob(ctx, jobFn, rc.Params{ 280 "_config": `{"BufferSize": "42M"}`, 281 }) 282 require.NoError(t, err) 283 assert.Equal(t, true, called) 284 // Check that wasn't the default 285 ci := fs.GetConfig(ctx) 286 assert.NotEqual(t, 42*fs.Mebi, ci.BufferSize) 287 } 288 289 func TestExecuteJobWithFilter(t *testing.T) { 290 ctx := context.Background() 291 called := false 292 jobID.Store(0) 293 jobFn := func(ctx context.Context, in rc.Params) (rc.Params, error) { 294 fi := filter.GetConfig(ctx) 295 assert.Equal(t, fs.SizeSuffix(1024), fi.Opt.MaxSize) 296 assert.Equal(t, []string{"a", "b", "c"}, fi.Opt.IncludeRule) 297 called = true 298 return nil, nil 299 } 300 _, _, err := NewJob(ctx, jobFn, rc.Params{ 301 "_filter": rc.Params{ 302 "IncludeRule": []string{"a", "b", "c"}, 303 "MaxSize": "1k", 304 }, 305 }) 306 require.NoError(t, err) 307 assert.Equal(t, true, called) 308 } 309 310 func TestExecuteJobWithGroup(t *testing.T) { 311 ctx := context.Background() 312 jobID.Store(0) 313 called := false 314 jobFn := func(ctx context.Context, in rc.Params) (rc.Params, error) { 315 called = true 316 group, found := accounting.StatsGroupFromContext(ctx) 317 assert.Equal(t, true, found) 318 assert.Equal(t, "myparty", group) 319 return nil, nil 320 } 321 _, _, err := NewJob(ctx, jobFn, rc.Params{ 322 "_group": "myparty", 323 }) 324 require.NoError(t, err) 325 assert.Equal(t, true, called) 326 } 327 328 func TestExecuteJobErrorPropagation(t *testing.T) { 329 ctx := context.Background() 330 jobID.Store(0) 331 332 testErr := errors.New("test error") 333 errorFn := func(ctx context.Context, in rc.Params) (out rc.Params, err error) { 334 return nil, testErr 335 } 336 _, _, err := NewJob(ctx, errorFn, rc.Params{}) 337 assert.Equal(t, testErr, err) 338 } 339 340 func TestRcJobStatus(t *testing.T) { 341 ctx := context.Background() 342 jobID.Store(0) 343 _, _, err := NewJob(ctx, longFn, rc.Params{"_async": true}) 344 assert.NoError(t, err) 345 346 call := rc.Calls.Get("job/status") 347 assert.NotNil(t, call) 348 in := rc.Params{"jobid": 1} 349 out, err := call.Fn(context.Background(), in) 350 require.NoError(t, err) 351 require.NotNil(t, out) 352 assert.Equal(t, float64(1), out["id"]) 353 assert.Equal(t, "", out["error"]) 354 assert.Equal(t, false, out["finished"]) 355 assert.Equal(t, false, out["success"]) 356 357 in = rc.Params{"jobid": 123123123} 358 _, err = call.Fn(context.Background(), in) 359 require.Error(t, err) 360 assert.Contains(t, err.Error(), "job not found") 361 362 in = rc.Params{"jobidx": 123123123} 363 _, err = call.Fn(context.Background(), in) 364 require.Error(t, err) 365 assert.Contains(t, err.Error(), "Didn't find key") 366 } 367 368 func TestRcJobList(t *testing.T) { 369 ctx := context.Background() 370 jobID.Store(0) 371 _, _, err := NewJob(ctx, longFn, rc.Params{"_async": true}) 372 assert.NoError(t, err) 373 374 call := rc.Calls.Get("job/list") 375 assert.NotNil(t, call) 376 in := rc.Params{} 377 out1, err := call.Fn(context.Background(), in) 378 require.NoError(t, err) 379 require.NotNil(t, out1) 380 assert.Equal(t, []int64{1}, out1["jobids"], "should have job listed") 381 382 _, _, err = NewJob(ctx, longFn, rc.Params{"_async": true}) 383 assert.NoError(t, err) 384 385 call = rc.Calls.Get("job/list") 386 assert.NotNil(t, call) 387 in = rc.Params{} 388 out2, err := call.Fn(context.Background(), in) 389 require.NoError(t, err) 390 require.NotNil(t, out2) 391 assert.Equal(t, 2, len(out2["jobids"].([]int64)), "should have all jobs listed") 392 393 require.NotNil(t, out1["executeId"], "should have executeId") 394 assert.Equal(t, out1["executeId"], out2["executeId"], "executeId should be the same") 395 } 396 397 func TestRcAsyncJobStop(t *testing.T) { 398 ctx := context.Background() 399 jobID.Store(0) 400 _, _, err := NewJob(ctx, ctxFn, rc.Params{"_async": true}) 401 assert.NoError(t, err) 402 403 call := rc.Calls.Get("job/stop") 404 assert.NotNil(t, call) 405 in := rc.Params{"jobid": 1} 406 out, err := call.Fn(context.Background(), in) 407 require.NoError(t, err) 408 require.Empty(t, out) 409 410 in = rc.Params{"jobid": 123123123} 411 _, err = call.Fn(context.Background(), in) 412 require.Error(t, err) 413 assert.Contains(t, err.Error(), "job not found") 414 415 in = rc.Params{"jobidx": 123123123} 416 _, err = call.Fn(context.Background(), in) 417 require.Error(t, err) 418 assert.Contains(t, err.Error(), "Didn't find key") 419 420 time.Sleep(10 * time.Millisecond) 421 422 call = rc.Calls.Get("job/status") 423 assert.NotNil(t, call) 424 in = rc.Params{"jobid": 1} 425 out, err = call.Fn(context.Background(), in) 426 require.NoError(t, err) 427 require.NotNil(t, out) 428 assert.Equal(t, float64(1), out["id"]) 429 assert.Equal(t, "context canceled", out["error"]) 430 assert.Equal(t, true, out["finished"]) 431 assert.Equal(t, false, out["success"]) 432 } 433 434 func TestRcSyncJobStop(t *testing.T) { 435 ctx, cancel := context.WithCancel(context.Background()) 436 go func() { 437 jobID.Store(0) 438 job, out, err := NewJob(ctx, ctxFn, rc.Params{}) 439 assert.Error(t, err) 440 assert.Equal(t, int64(1), job.ID) 441 assert.Equal(t, rc.Params{}, out) 442 }() 443 444 time.Sleep(10 * time.Millisecond) 445 446 call := rc.Calls.Get("job/stop") 447 assert.NotNil(t, call) 448 in := rc.Params{"jobid": 1} 449 out, err := call.Fn(context.Background(), in) 450 require.NoError(t, err) 451 require.Empty(t, out) 452 453 in = rc.Params{"jobid": 123123123} 454 _, err = call.Fn(context.Background(), in) 455 require.Error(t, err) 456 assert.Contains(t, err.Error(), "job not found") 457 458 in = rc.Params{"jobidx": 123123123} 459 _, err = call.Fn(context.Background(), in) 460 require.Error(t, err) 461 assert.Contains(t, err.Error(), "Didn't find key") 462 463 cancel() 464 time.Sleep(10 * time.Millisecond) 465 466 call = rc.Calls.Get("job/status") 467 assert.NotNil(t, call) 468 in = rc.Params{"jobid": 1} 469 out, err = call.Fn(context.Background(), in) 470 require.NoError(t, err) 471 require.NotNil(t, out) 472 assert.Equal(t, float64(1), out["id"]) 473 assert.Equal(t, "context canceled", out["error"]) 474 assert.Equal(t, true, out["finished"]) 475 assert.Equal(t, false, out["success"]) 476 } 477 478 func TestRcJobStopGroup(t *testing.T) { 479 ctx := context.Background() 480 jobID.Store(0) 481 _, _, err := NewJob(ctx, ctxFn, rc.Params{ 482 "_async": true, 483 "_group": "myparty", 484 }) 485 require.NoError(t, err) 486 _, _, err = NewJob(ctx, ctxFn, rc.Params{ 487 "_async": true, 488 "_group": "myparty", 489 }) 490 require.NoError(t, err) 491 492 call := rc.Calls.Get("job/stopgroup") 493 assert.NotNil(t, call) 494 in := rc.Params{"group": "myparty"} 495 out, err := call.Fn(context.Background(), in) 496 require.NoError(t, err) 497 require.Empty(t, out) 498 499 in = rc.Params{} 500 _, err = call.Fn(context.Background(), in) 501 require.Error(t, err) 502 assert.Contains(t, err.Error(), "Didn't find key") 503 504 time.Sleep(10 * time.Millisecond) 505 506 call = rc.Calls.Get("job/status") 507 assert.NotNil(t, call) 508 for i := 1; i <= 2; i++ { 509 in = rc.Params{"jobid": i} 510 out, err = call.Fn(context.Background(), in) 511 require.NoError(t, err) 512 require.NotNil(t, out) 513 assert.Equal(t, "myparty", out["group"]) 514 assert.Equal(t, "context canceled", out["error"]) 515 assert.Equal(t, true, out["finished"]) 516 assert.Equal(t, false, out["success"]) 517 } 518 } 519 520 func TestOnFinish(t *testing.T) { 521 jobID.Store(0) 522 done := make(chan struct{}) 523 ctx, cancel := context.WithCancel(context.Background()) 524 job, _, err := NewJob(ctx, ctxParmFn(ctx, false), rc.Params{"_async": true}) 525 assert.NoError(t, err) 526 527 stop, err := OnFinish(job.ID, func() { close(done) }) 528 defer stop() 529 assert.NoError(t, err) 530 531 cancel() 532 533 select { 534 case <-done: 535 case <-time.After(time.Second): 536 t.Fatal("Timeout waiting for OnFinish to fire") 537 } 538 } 539 540 func TestOnFinishAlreadyFinished(t *testing.T) { 541 jobID.Store(0) 542 done := make(chan struct{}) 543 ctx, cancel := context.WithCancel(context.Background()) 544 defer cancel() 545 job, _, err := NewJob(ctx, shortFn, rc.Params{}) 546 assert.NoError(t, err) 547 548 stop, err := OnFinish(job.ID, func() { close(done) }) 549 defer stop() 550 assert.NoError(t, err) 551 552 select { 553 case <-done: 554 case <-time.After(time.Second): 555 t.Fatal("Timeout waiting for OnFinish to fire") 556 } 557 }