|
5 | 5 | "path/filepath" |
6 | 6 | "strings" |
7 | 7 | "testing" |
| 8 | + "time" |
8 | 9 |
|
9 | 10 | "github.com/jkleinne/shuttle/internal/config" |
10 | 11 | ) |
@@ -546,3 +547,155 @@ destination = "/tmp/backup" |
546 | 547 | t.Error("Jobs[0].Optional = true, want false (zero value)") |
547 | 548 | } |
548 | 549 | } |
| 550 | + |
| 551 | +func TestLoad_MaxRuntime_ParsedAndExposed(t *testing.T) { |
| 552 | + tomlData := ` |
| 553 | +[[job]] |
| 554 | +name = "photos" |
| 555 | +engine = "rsync" |
| 556 | +sources = ["/tmp/photos"] |
| 557 | +destination = "/tmp/backup" |
| 558 | +max_runtime = "2h" |
| 559 | +` |
| 560 | + cfg, err := config.LoadBytes([]byte(tomlData)) |
| 561 | + if err != nil { |
| 562 | + t.Fatalf("unexpected error: %v", err) |
| 563 | + } |
| 564 | + if cfg.Jobs[0].MaxRuntime != "2h" { |
| 565 | + t.Errorf("MaxRuntime = %q, want \"2h\"", cfg.Jobs[0].MaxRuntime) |
| 566 | + } |
| 567 | + got, err := cfg.Jobs[0].MaxRuntimeDuration() |
| 568 | + if err != nil { |
| 569 | + t.Fatalf("MaxRuntimeDuration() error = %v", err) |
| 570 | + } |
| 571 | + if got != 2*time.Hour { |
| 572 | + t.Errorf("MaxRuntimeDuration() = %v, want 2h", got) |
| 573 | + } |
| 574 | +} |
| 575 | + |
| 576 | +func TestLoad_MaxRuntime_Absent_DurationIsZero(t *testing.T) { |
| 577 | + tomlData := ` |
| 578 | +[[job]] |
| 579 | +name = "photos" |
| 580 | +engine = "rsync" |
| 581 | +sources = ["/tmp/photos"] |
| 582 | +destination = "/tmp/backup" |
| 583 | +` |
| 584 | + cfg, err := config.LoadBytes([]byte(tomlData)) |
| 585 | + if err != nil { |
| 586 | + t.Fatalf("unexpected error: %v", err) |
| 587 | + } |
| 588 | + if cfg.Jobs[0].MaxRuntime != "" { |
| 589 | + t.Errorf("MaxRuntime = %q, want empty", cfg.Jobs[0].MaxRuntime) |
| 590 | + } |
| 591 | + got, err := cfg.Jobs[0].MaxRuntimeDuration() |
| 592 | + if err != nil { |
| 593 | + t.Fatalf("MaxRuntimeDuration() error = %v", err) |
| 594 | + } |
| 595 | + if got != 0 { |
| 596 | + t.Errorf("MaxRuntimeDuration() = %v, want 0", got) |
| 597 | + } |
| 598 | +} |
| 599 | + |
| 600 | +func TestLoad_MaxRuntime_Zero_Rejected(t *testing.T) { |
| 601 | + tomlData := ` |
| 602 | +[[job]] |
| 603 | +name = "photos" |
| 604 | +engine = "rsync" |
| 605 | +sources = ["/tmp/photos"] |
| 606 | +destination = "/tmp/backup" |
| 607 | +max_runtime = "0s" |
| 608 | +` |
| 609 | + _, err := config.LoadBytes([]byte(tomlData)) |
| 610 | + if err == nil { |
| 611 | + t.Fatal("expected error for max_runtime = \"0s\", got nil") |
| 612 | + } |
| 613 | + msg := err.Error() |
| 614 | + if !strings.Contains(msg, "photos") || !strings.Contains(msg, "max_runtime") { |
| 615 | + t.Errorf("error %q should mention job name and field", msg) |
| 616 | + } |
| 617 | +} |
| 618 | + |
| 619 | +func TestLoad_MaxRuntime_Negative_Rejected(t *testing.T) { |
| 620 | + tomlData := ` |
| 621 | +[[job]] |
| 622 | +name = "photos" |
| 623 | +engine = "rsync" |
| 624 | +sources = ["/tmp/photos"] |
| 625 | +destination = "/tmp/backup" |
| 626 | +max_runtime = "-5m" |
| 627 | +` |
| 628 | + _, err := config.LoadBytes([]byte(tomlData)) |
| 629 | + if err == nil { |
| 630 | + t.Fatal("expected error for negative max_runtime, got nil") |
| 631 | + } |
| 632 | + if !strings.Contains(err.Error(), "photos") { |
| 633 | + t.Errorf("error %q should mention job name", err.Error()) |
| 634 | + } |
| 635 | + if !strings.Contains(err.Error(), "positive") { |
| 636 | + t.Errorf("error %q should mention \"positive\"", err.Error()) |
| 637 | + } |
| 638 | +} |
| 639 | + |
| 640 | +func TestLoad_MaxRuntime_Malformed_Rejected(t *testing.T) { |
| 641 | + tomlData := ` |
| 642 | +[[job]] |
| 643 | +name = "photos" |
| 644 | +engine = "rsync" |
| 645 | +sources = ["/tmp/photos"] |
| 646 | +destination = "/tmp/backup" |
| 647 | +max_runtime = "banana" |
| 648 | +` |
| 649 | + _, err := config.LoadBytes([]byte(tomlData)) |
| 650 | + if err == nil { |
| 651 | + t.Fatal("expected error for malformed max_runtime, got nil") |
| 652 | + } |
| 653 | + if !strings.Contains(err.Error(), "photos") { |
| 654 | + t.Errorf("error %q should mention job name", err.Error()) |
| 655 | + } |
| 656 | +} |
| 657 | + |
| 658 | +func TestLoad_MaxRuntime_RcloneJob_ValidatedAndExposed(t *testing.T) { |
| 659 | + // validateMaxRuntime is called from both validateRsyncJob and |
| 660 | + // validateRcloneJob. The other tests cover the rsync path; this |
| 661 | + // pins that the validator runs for rclone jobs too. |
| 662 | + tomlData := ` |
| 663 | +[[job]] |
| 664 | +name = "docs-to-cloud" |
| 665 | +engine = "rclone" |
| 666 | +source = "/tmp/docs" |
| 667 | +remotes = ["my_gdrive"] |
| 668 | +mode = "copy" |
| 669 | +max_runtime = "30m" |
| 670 | +` |
| 671 | + cfg, err := config.LoadBytes([]byte(tomlData)) |
| 672 | + if err != nil { |
| 673 | + t.Fatalf("unexpected error: %v", err) |
| 674 | + } |
| 675 | + got, err := cfg.Jobs[0].MaxRuntimeDuration() |
| 676 | + if err != nil { |
| 677 | + t.Fatalf("MaxRuntimeDuration() error = %v", err) |
| 678 | + } |
| 679 | + if got != 30*time.Minute { |
| 680 | + t.Errorf("MaxRuntimeDuration() = %v, want 30m", got) |
| 681 | + } |
| 682 | +} |
| 683 | + |
| 684 | +func TestLoad_MaxRuntime_RcloneJob_NegativeRejected(t *testing.T) { |
| 685 | + tomlData := ` |
| 686 | +[[job]] |
| 687 | +name = "docs-to-cloud" |
| 688 | +engine = "rclone" |
| 689 | +source = "/tmp/docs" |
| 690 | +remotes = ["my_gdrive"] |
| 691 | +mode = "copy" |
| 692 | +max_runtime = "-1h" |
| 693 | +` |
| 694 | + _, err := config.LoadBytes([]byte(tomlData)) |
| 695 | + if err == nil { |
| 696 | + t.Fatal("expected error for negative max_runtime on rclone job, got nil") |
| 697 | + } |
| 698 | + if !strings.Contains(err.Error(), "docs-to-cloud") { |
| 699 | + t.Errorf("error %q should mention job name", err.Error()) |
| 700 | + } |
| 701 | +} |
0 commit comments