Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduling improvements #425

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open

Conversation

creativeprojects
Copy link
Owner

Improvements on the scheduling:

  • resticprofile is now capable of reading back the existing scheduled configuration and update what has changed in the configuration file (works for all scheduling methods)

Fixes #378

Copy link

coderabbitai bot commented Nov 1, 2024

Walkthrough

The changes integrate golangci-lint into the build process and refine the event scheduling logic to account for non-zero seconds, thereby preventing negative “from now” outputs. Multiple packages have been overhauled to improve configuration loading, error handling, logging, and scheduling job management. Enhancements span Makefile targets; calendar, commands, crontab, schedule, schtasks, shell, and systemd modules; and associated tests, ensuring stricter initialisation and more robust unit and integration tests.

Changes

Files Change Summary
Makefile, config/config.go, config/load_options.go, codecov.yml, examples/linux.yaml, examples/v2.yaml Added golangci-lint target; updated configuration loading to accept options; enhanced example configurations; and updated code coverage ignore patterns.
calendar/event.go, calendar/event_test.go Adjusted the Next method to increment the scheduled event if seconds are non-zero, ensuring events are not computed in the past; updated tests with explicit reference times.
commands.go, commands_schedule.go, commands_schedule_test.go, commands_test.go, complete_test.go Introduced a sync.Once mechanism for privilege elevation; updated command targets to include a new flag (--start) and variable renames; and streamlined error handling in scheduling commands and tests.
crond/* (crontab.go, crontab_test.go, entry.go, io.go, parse_event.go, parse_event_test.go) Enhanced crontab processing with new regular expressions and filesystem abstraction via afero; added getter methods for Entry; introduced event parsing functions and corresponding tests.
schedule/* (across numerous schedule module files) Overhauled scheduling module with updated method signatures (e.g. DisplaySchedules now accepts a profile), new functions for calendar intervals and command argument trimming, refactored tests to use stricter assertions, and removed deprecated fields from Config.
schtasks/, shell/split_arguments, systemd/* Introduced a Registered function in schtasks for task registration; added a command line argument splitter in shell; and improved systemd unit file handling (reading, generating, logging) with corresponding tests.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant B as Build System
    participant H as Scheduler Handler
    participant J as Job Module
    participant E as Calendar Event

    U->>B: Trigger build (via Makefile targets)
    B-->>U: Ensure golangci-lint is installed
    U->>H: Execute scheduling command
    H->>E: Request next event time calculation
    E-->>H: Return adjusted next event time (increments minute if seconds > 0)
    H->>J: Create new job using updated configuration
    J->>H: Confirm job creation and display schedules (--start flag included)
    H-->>U: Return scheduling status and job details
Loading

Assessment against linked issues

Objective Addressed Explanation
[#378] Correct rounding in next event computation to avoid negative time remaining

Tip

🌐 Web search-backed reviews and chat
  • We have enabled web search-based reviews and chat for all users. This feature allows CodeRabbit to access the latest documentation and information on the web.
  • You can disable this feature by setting web_search: false in the knowledge_base settings.
  • Please share any feedback in the Discord discussion.
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@creativeprojects creativeprojects added the enhancement New feature or request label Nov 1, 2024
@creativeprojects creativeprojects added this to the v0.30.0 milestone Nov 1, 2024
Copy link

codecov bot commented Nov 2, 2024

Codecov Report

Attention: Patch coverage is 73.34526% with 298 lines in your changes missing coverage. Please review.

Project coverage is 78.79%. Comparing base (e2297a6) to head (003d987).

Files with missing lines Patch % Lines
commands_schedule.go 42.16% 55 Missing and 4 partials ⚠️
schedule/handler_systemd.go 59.44% 43 Missing and 15 partials ⚠️
schedule/handler_darwin.go 66.88% 40 Missing and 11 partials ⚠️
schedule/handler_windows.go 3.57% 27 Missing ⚠️
schedule_jobs.go 85.19% 10 Missing and 6 partials ⚠️
systemd/read.go 83.33% 11 Missing and 5 partials ⚠️
crond/crontab.go 82.76% 10 Missing and 5 partials ⚠️
schedule/handler_crond.go 76.36% 10 Missing and 3 partials ⚠️
schtasks/taskscheduler.go 69.70% 7 Missing and 3 partials ⚠️
crond/parse_event.go 86.76% 6 Missing and 3 partials ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #425      +/-   ##
==========================================
+ Coverage   72.23%   78.79%   +6.56%     
==========================================
  Files         129      128       -1     
  Lines       13796    12893     -903     
==========================================
+ Hits         9965    10158     +193     
+ Misses       3392     2344    -1048     
+ Partials      439      391      -48     
Flag Coverage Δ
unittests 78.79% <73.35%> (+6.56%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@creativeprojects creativeprojects marked this pull request as ready for review February 4, 2025 13:34
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🔭 Outside diff range comments (1)
schedule/config.go (1)

30-36: Update NewRemoveOnlyConfig method to align with struct changes

This method still sets the removeOnly field which may cause compilation errors if the field is indeed meant to be removed from the struct.

Please update this method to align with any changes to the struct definition.

🧹 Nitpick comments (43)
schedule/config.go (1)

40-44: Consider adding input validation

The SetCommand method accepts parameters without validation.

Consider adding validation for:

  • Empty working directory
  • Empty or invalid command
  • Nil arguments slice
 func (s *Config) SetCommand(wd, command string, args []string) {
+	if wd == "" {
+		wd = "."
+	}
+	if command == "" {
+		return
+	}
+	if args == nil {
+		args = []string{}
+	}
 	s.WorkingDirectory = wd
 	s.Command = command
 	s.Arguments = NewCommandArguments(args)
 }
schedule/config_test.go (1)

9-40: Enhance test coverage

The test suite could benefit from additional test cases.

Consider adding tests for:

  1. SetCommand method with various inputs
  2. Error cases and edge cases
  3. NewRemoveOnlyConfig method

Example test for SetCommand:

func TestSetCommand(t *testing.T) {
	tests := []struct {
		name    string
		wd      string
		command string
		args    []string
		want    CommandArguments
	}{
		{
			name:    "empty working directory",
			wd:      "",
			command: "test",
			args:    []string{"arg"},
			want:    NewCommandArguments([]string{"arg"}),
		},
		// Add more test cases
	}
	
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &Config{}
			s.SetCommand(tt.wd, tt.command, tt.args)
			assert.Equal(t, tt.want, s.Arguments)
		})
	}
}
schedule/spaced_title.go (1)

7-18: Add documentation for the spacedTitle function.

The function lacks documentation explaining its purpose and behaviour. Consider adding a comment block describing:

  • Function purpose
  • Parameter expectations
  • Return value format
  • Example usage
+// spacedTitle adds spaces before capital letters in a string, except for the first character
+// or when the capital letter follows a space. For example, "ThisIsATest" becomes "This Is A Test".
+// If the input is empty or contains no capital letters, it is returned unchanged.
 func spacedTitle(title string) string {
schedule/spaced_title_test.go (1)

12-16: Consider additional test cases for comprehensive coverage.

While the current test cases are good, consider adding:

  • Mixed case words (e.g., "iPhone" → "i Phone")
  • Numbers (e.g., "iOS15Device" → "i OS15 Device")
  • Special characters (e.g., "user@Home" → "user@ Home")
 	}{
 		{"NoSpacesHere", "No Spaces Here"},
 		{"Already Spaced", "Already Spaced"},
 		{"", ""},
 		{"lowercase", "lowercase"},
 		{"ALLCAPS", "A L L C A P S"},
+		{"iPhone", "i Phone"},
+		{"iOS15Device", "i OS15 Device"},
+		{"user@Home", "user@ Home"},
 	}
schedule/calendar_interval_test.go (1)

11-55: Well-structured table-driven tests with good coverage.

The test cases are well-organised and cover the basic scenarios. However, consider adding these test cases:

  • Invalid intervals with out-of-range values (e.g., minute > 59)
  • Intervals with overlapping values
  • Intervals with all fields populated vs sparse intervals
 		{
 			name:      "Empty intervals",
 			intervals: []CalendarInterval{},
 			expected:  []string{"*-*-* *:*:00"},
 		},
+		{
+			name: "Invalid interval",
+			intervals: []CalendarInterval{
+				{
+					intervalMinute: 60,  // Invalid: minute > 59
+					intervalHour:   24,  // Invalid: hour > 23
+				},
+			},
+			expected: []string{"*-*-* *:*:00"},
+		},
+		{
+			name: "All fields populated",
+			intervals: []CalendarInterval{
+				{
+					intervalMinute:  15,
+					intervalHour:    9,
+					intervalWeekday: 1,
+					intervalDay:     1,
+					intervalMonth:   1,
+				},
+			},
+			expected: []string{"Mon *-01-01 09:15:00"},
+		},
schedule/calendar_interval.go (2)

7-23: Consider using iota for constants and adding value validation.

The constants and type definition are well-documented. However:

  1. Consider using iota for the interval constants to ensure uniqueness.
  2. Add value validation to ensure the map values are within valid ranges (e.g., 0-59 for minutes).
 const (
-	intervalMinute  = "Minute"
-	intervalHour    = "Hour"
-	intervalWeekday = "Weekday"
-	intervalDay     = "Day"
-	intervalMonth   = "Month"
+	intervalMinute CalendarIntervalType = iota
+	intervalHour
+	intervalWeekday
+	intervalDay
+	intervalMonth
 )
+
+type CalendarIntervalType int
+
+func (t CalendarIntervalType) String() string {
+	return [...]string{"Minute", "Hour", "Weekday", "Day", "Month"}[t]
+}
+
+func (t CalendarIntervalType) isValid(value int) bool {
+	switch t {
+	case intervalMinute:
+		return value >= 0 && value <= 59
+	case intervalHour:
+		return value >= 0 && value <= 23
+	case intervalWeekday:
+		return value >= 0 && value <= 7
+	case intervalDay:
+		return value >= 1 && value <= 31
+	case intervalMonth:
+		return value >= 1 && value <= 12
+	default:
+		return false
+	}
+}

67-73: Consider pre-allocating the slice with a more accurate capacity.

The slice is pre-allocated with the length of schedules, but each schedule could generate multiple intervals.

 func getCalendarIntervalsFromSchedules(schedules []*calendar.Event) []CalendarInterval {
-	entries := make([]CalendarInterval, 0, len(schedules))
+	// Estimate capacity: each schedule might generate multiple intervals
+	estimatedCapacity := len(schedules) * 2 // or another reasonable multiplier
+	entries := make([]CalendarInterval, 0, estimatedCapacity)
 	for _, schedule := range schedules {
 		entries = append(entries, getCalendarIntervalsFromScheduleTree(generateTreeOfSchedules(schedule))...)
 	}
 	return entries
 }
systemd/read.go (2)

47-47: Consider making the priority configurable instead of hard-coded
This hard-coded "background" value may not suit all use-cases or environments.


86-89: Handle strconv.Atoi parse errors
Currently, parse errors are silently ignored, defaulting to 0. This may cause unexpected behaviour if the user provides a non-integer value. Consider at least logging or returning an error to highlight invalid values.

schedule/handler_systemd.go (3)

97-99: Avoid silently discarding errors
These lines ignore any existing error from getting the systemd status and return nil. This could hinder troubleshooting if the error is legitimate.


223-225: Check return value of runSystemctlReload
The reload result is discarded, which might mask issues if systemd fails to reload. Consider capturing or logging the error.


244-244: Consider handling runJournalCtlCommand error
Ignoring the error may hide potential problems that occur when retrieving the logs. Logging it could assist debugging.

shell/split_arguments.go (1)

9-32: Add documentation and input validation.

The function logic is correct, but could benefit from the following improvements:

  1. Add documentation to explain the function's behaviour with quoted strings and escape characters.
  2. Add input validation for empty or nil strings.
  3. Add examples to demonstrate usage.

Apply this diff to add documentation and input validation:

+// SplitArguments splits a command line string into individual arguments.
+// It handles quoted strings and escape characters, with platform-specific behaviour:
+// - On non-Windows platforms, backslashes are treated as escape characters
+// - Quoted strings are preserved as single arguments
+// - Spaces outside quotes are used as argument delimiters
+//
+// Example:
+//
+//	SplitArguments(`echo "Hello World" file\ with\ spaces`)
+//	// Returns: []string{"echo", "Hello World", "file with spaces"}
 func SplitArguments(commandLine string) []string {
+	if len(commandLine) == 0 {
+		return []string{}
+	}
 	args := make([]string, 0)
crond/parse_event_test.go (1)

10-46: Add test cases for invalid inputs and edge cases.

The test function has good coverage of valid cron expressions, but could benefit from additional test cases:

  1. Invalid cron expressions
  2. Edge cases (empty string, invalid day of week, etc.)
  3. Comments explaining complex test cases

Apply this diff to add more test cases:

 	testData := []struct {
 		cron     string
 		expected string
+		wantErr  bool
 	}{
 		{"00 00 * * 1,2,3,4,6,0", "Sun..Thu,Sat *-*-* 00:00:00"},
 		{"23 01,02 * * 1,0", "Sun,Mon *-*-* 01,02:23:00"},
+		// Invalid inputs
+		{"", "", true},
+		{"invalid", "", true},
+		{"99 99 * * *", "", true},
+		// Edge cases
+		{"59 23 31 12 7", "", true},  // Invalid day of week
+		{"59 23 31 13 6", "", true},  // Invalid month
 	}

 	for _, testRun := range testData {
 		t.Run(testRun.cron, func(t *testing.T) {
 			event, err := parseEvent(testRun.cron)
+			if testRun.wantErr {
+				require.Error(t, err)
+				return
+			}
 			require.NoError(t, err)
 			assert.Equal(t, testRun.expected, event.String())
 		})
 	}
schedule/command_arguments.go (1)

57-70: Consider edge cases in ConfigFile method.

While the implementation is generally good, it might be worth handling cases where the --config flag appears multiple times in the arguments.

Consider this enhancement:

 func (ca CommandArguments) ConfigFile() string {
 	if len(ca.args) == 0 {
 		return ""
 	}
+	var lastConfig string
 	for i, arg := range ca.args {
 		if arg == "--config" {
 			if i+1 < len(ca.args) {
-				return ca.args[i+1]
+				lastConfig = ca.args[i+1]
 			}
 		}
 	}
-	return ""
+	return lastConfig
 }
crond/parse_event.go (1)

26-37: Consider using a named slice for better readability.

The slice of event fields could be defined as a named variable for better readability and maintainability.

Apply this diff to improve readability:

-	for index, eventField := range []*calendar.Value{
+	eventFields := []*calendar.Value{
 		event.Minute,
 		event.Hour,
 		event.Day,
 		event.Month,
 		event.WeekDay,
-	} {
+	}
+	for index, eventField := range eventFields {
shell/split_arguments_test.go (2)

11-64: Consider grouping test cases by functionality.

The test cases cover different scenarios (basic arguments, quoted strings, platform-specific paths) but are not clearly grouped. Consider using subtests to group related test cases for better organisation and readability.

Apply this diff to improve test structure:

 func TestSplitArguments(t *testing.T) {
-	testCases := []struct {
+	// Basic argument splitting
+	basicCases := []struct {
 		commandLine  string
 		expectedArgs []string
 		windowsMode  bool
 		unixMode     bool
 	}{
 		{
 			commandLine:  `cmd arg1 arg2`,
 			expectedArgs: []string{"cmd", "arg1", "arg2"},
 		},
+	}
+
+	// Quoted string handling
+	quotedCases := []struct {
+		commandLine  string
+		expectedArgs []string
+		windowsMode  bool
+		unixMode     bool
+	}{
 		{
 			commandLine:  `cmd "arg with spaces" arg3`,
 			expectedArgs: []string{"cmd", "arg with spaces", "arg3"},
 		},
+	}
+
+	// Platform-specific cases
+	platformCases := []struct {
+		commandLine  string
+		expectedArgs []string
+		windowsMode  bool
+		unixMode     bool
+	}{
 		{
 			commandLine:  `args --with folder\file.txt`,
 			expectedArgs: []string{"args", "--with", "folder\\file.txt"},
 			windowsMode:  true,
 		},
 	}

73-76: Consider using more descriptive test names.

The current test names use the command line as the test name, which can be hard to read. Consider using descriptive names that explain the test's purpose.

Apply this diff to improve test names:

-		t.Run(testCase.commandLine, func(t *testing.T) {
+		testName := fmt.Sprintf("%s/%s",
+			getPlatformPrefix(testCase.windowsMode, testCase.unixMode),
+			getTestDescription(testCase.commandLine))
+		t.Run(testName, func(t *testing.T) {

Add these helper functions:

func getPlatformPrefix(windowsMode, unixMode bool) string {
	if windowsMode {
		return "Windows"
	}
	if unixMode {
		return "Unix"
	}
	return "Common"
}

func getTestDescription(commandLine string) string {
	if strings.Contains(commandLine, "\"") {
		return "QuotedArguments"
	}
	if strings.Contains(commandLine, "\\") {
		return "EscapedPath"
	}
	return "BasicArguments"
}
schedule/handler_crond_test.go (1)

19-51: Consider adding error test cases.

The test cases only cover successful scenarios. Consider adding test cases for error conditions such as invalid schedules, permission issues, or filesystem errors.

Add test cases for error scenarios:

{
	job: Config{
		ProfileName: "invalid",
		Schedules:   []string{"invalid schedule"},
	},
	schedules:    nil,
	expectError:  true,
	errorMessage: "invalid schedule format",
},
{
	job: Config{
		ProfileName: "no.permission",
		Permission:  "root",
	},
	schedules:    []*calendar.Event{hourly},
	expectError:  true,
	errorMessage: "permission denied",
},
schedule/job.go (1)

11-11: Make the error message more descriptive.

The error message "job can be removed only" could be more descriptive to help users understand why the operation failed.

Apply this diff to improve the error message:

-var ErrJobCanBeRemovedOnly = errors.New("job can be removed only")
+var ErrJobCanBeRemovedOnly = errors.New("this job is marked for removal only and cannot be created or modified")
schedule/schedules.go (2)

44-56: Consider using string builder for performance.

While the current implementation is correct, using strings.Builder for concatenating multiple strings in the loop could be more efficient, especially when dealing with many events.

-func displayParsedSchedules(profile, command string, events []*calendar.Event) {
+func displayParsedSchedules(profile, command string, events []*calendar.Event) {
 	now := time.Now().Round(time.Second)
+	var output strings.Builder
 	for index, event := range events {
 		displayHeader(profile, command, index+1, len(events))
 		next := event.Next(now)
-		term.Printf("  Original form: %s\n", event.Input())
-		term.Printf("Normalized form: %s\n", event.String())
-		term.Printf("    Next elapse: %s\n", next.Format(time.UnixDate))
-		term.Printf("       (in UTC): %s\n", next.UTC().Format(time.UnixDate))
-		term.Printf("       From now: %s left\n", next.Sub(now))
+		fmt.Fprintf(&output, "  Original form: %s\n", event.Input())
+		fmt.Fprintf(&output, "Normalized form: %s\n", event.String())
+		fmt.Fprintf(&output, "    Next elapse: %s\n", next.Format(time.UnixDate))
+		fmt.Fprintf(&output, "       (in UTC): %s\n", next.UTC().Format(time.UnixDate))
+		fmt.Fprintf(&output, "       From now: %s left\n", next.Sub(now))
+		term.Print(output.String())
+		output.Reset()
 	}
 	term.Print(platform.LineSeparator)
 }

58-74: Consider error handling enhancement.

The error handling for the systemd-analyze command could be more informative by including the schedule that caused the error.

-			return err
+			return fmt.Errorf("failed to analyze schedule %q: %w", schedule, err)
systemd/drop_ins.go (1)

68-69: Good improvements to logging and resource management!

The changes to use Debug level for file operations and the addition of defer statements for file closing are excellent improvements. However, consider handling errors from file closing operations.

-		defer dst.Close()
+		defer func() {
+			if cerr := dst.Close(); cerr != nil && err == nil {
+				err = fmt.Errorf("error closing destination file: %w", cerr)
+			}
+		}()

-		defer src.Close()
+		defer func() {
+			if cerr := src.Close(); cerr != nil && err == nil {
+				err = fmt.Errorf("error closing source file: %w", cerr)
+			}
+		}()

Also applies to: 81-93

schedule/handler_systemd_test.go (1)

15-85: Well-structured test with good cleanup and assertions!

The test is comprehensive and well-structured. However, consider extracting the test cases to a separate variable for better maintainability.

+var systemdTestCases = []struct {
+	job       Config
+	schedules []*calendar.Event
+}{
+	{
+		job: Config{
+			ProfileName:      "testscheduled",
+			CommandName:      "backup",
+			Command:          "/tmp/bin/resticprofile",
+			Arguments:        NewCommandArguments([]string{"--no-ansi", "--config", "examples/dev.yaml", "--name", "self", "check"}),
+			WorkingDirectory: "/resticprofile",
+			Permission:       schedulePermission,
+			ConfigFile:       "examples/dev.yaml",
+			Schedules:        []string{event.String()},
+		},
+		schedules: []*calendar.Event{event},
+	},
+	// ... other test cases
+}

 func TestReadingSystemdScheduled(t *testing.T) {
 	event := calendar.NewEvent()
 	require.NoError(t, event.Parse("2020-01-01"))

 	schedulePermission := constants.SchedulePermissionUser

-	testCases := []struct {
-		job       Config
-		schedules []*calendar.Event
-	}{
-		// ... test cases
-	}
+	testCases := systemdTestCases
commands_schedule_test.go (2)

40-40: Update test documentation to reflect new schedule format.

The schedule format has been changed from "daily" to "*:00,30", which runs every 30 minutes (at minutes 0 and 30 of every hour). Consider adding a comment to explain this schedule pattern for better maintainability.

  profile-schedule-inline:
    backup:
+     # Runs every 30 minutes (at minutes 0 and 30 of every hour)
      schedule: "*:00,30"

Also applies to: 45-45


79-79: Update test case descriptions.

The test case expectations have been updated to match the new schedule format, but the test case names could be more descriptive of what they're testing.

-			name: "",
+			name: "empty_profile",

Also applies to: 83-83, 87-87

schedule/schedules_test.go (1)

46-46: Update test documentation for new parameter.

The displayParsedSchedules function now accepts a profile parameter, but the test cases don't document why this parameter was added or how it affects the output.

Add comments explaining the purpose of the profile parameter:

+// TestDisplayParseSchedules verifies that schedules are correctly displayed
+// with profile information included in the output
 func TestDisplayParseSchedules(t *testing.T) {

Also applies to: 60-60, 82-82

schedule/command_arguments_test.go (1)

61-84: LGTM: Comprehensive test coverage for config file handling.

Excellent test coverage for the ConfigFile method. The test cases cover:

  • Empty arguments
  • Missing config value
  • Various path formats
  • Different argument positions
  • Interaction with other flags

Consider adding test cases for:

+		{[]string{"--config="}, ""},
+		{[]string{"--config=config.toml"}, "config.toml"},
crond/entry.go (1)

81-101: LGTM! Consider adding documentation.

The getter methods are well-structured and maintain proper encapsulation. Consider adding documentation comments to describe the purpose and return values of these methods.

Add documentation comments like this:

+// Event returns the calendar event associated with this entry
 func (e Entry) Event() *calendar.Event {
 	return e.event
 }
systemd/read_test.go (2)

43-75: Consider making test data more descriptive.

The test data could be more descriptive by:

  1. Using constants with meaningful names for test data
  2. Adding comments explaining the test scenarios

Example improvement:

+const (
+    // Test service unit with background priority and environment variables
+    testServiceUnit = `[Unit]
     Description=resticprofile copy for profile self in examples/linux.yaml
     OnFailure=unit-status-mail@%n.service

77-143: Add descriptions to table-driven test cases.

The test cases would be clearer with descriptions explaining each scenario.

Example improvement:

 testCases := []struct {
+    name   string
     config Config
 }{
     {
+        name: "Basic system unit with single schedule",
         config: Config{
             CommandLine:      "/bin/resticprofile --config profiles.yaml run-schedule backup@profile1",
calendar/event.go (1)

99-102: Add comment explaining the time adjustment logic.

The logic for handling non-zero seconds is correct but would benefit from a clear explanation.

Add a comment like this:

 next := from.Truncate(time.Minute) // truncate all the seconds
+// If we're already partway through a minute, skip to the next minute
+// to avoid scheduling events in the past
 if from.Second() > 0 {
     next = next.Add(time.Minute) // it's too late for the current minute
 }
schedule_jobs.go (2)

96-125: Consider adding error recovery for job removal.

The function continues after logging errors but doesn't track which jobs failed to be removed. Consider collecting and returning a slice of errors to provide better feedback to the caller.

 func removeScheduledJobs(handler schedule.Handler, configFile, profileName string) error {
+    var errors []error
     err := handler.Init()
     if err != nil {
         return err
     }
     defer handler.Close()

     clog.Debugf("looking up schedules from configuration file %s", configFile)
     configs, err := handler.Scheduled(profileName)
     if err != nil {
         return err
     }
     if len(configs) == 0 {
         clog.Info("no scheduled jobs found")
         return nil
     }
     for _, cfg := range configs {
         if cfg.ConfigFile != configFile {
             clog.Debugf("skipping job %s/%s from configuration file %s", cfg.ProfileName, cfg.CommandName, cfg.ConfigFile)
             continue
         }
         job := schedule.NewJob(handler, &cfg)
         err = job.Remove()
         if err != nil {
-            clog.Error(err)
+            errors = append(errors, fmt.Errorf("failed to remove job %s/%s: %w", cfg.ProfileName, cfg.CommandName, err))
         }
         clog.Infof("scheduled job %s/%s removed", cfg.ProfileName, cfg.CommandName)
     }
-    return nil
+    if len(errors) > 0 {
+        return fmt.Errorf("failed to remove some jobs: %v", errors)
+    }
+    return nil
 }

162-194: Consider adding error recovery for status checks.

Similar to removeScheduledJobs, the statusScheduledJobs function could benefit from collecting errors rather than just logging them.

 func statusScheduledJobs(handler schedule.Handler, configFile, profileName string) error {
+    var errors []error
     err := handler.Init()
     if err != nil {
         return err
     }
     defer handler.Close()

     clog.Debugf("looking up schedules from configuration file %s", configFile)
     configs, err := handler.Scheduled(profileName)
     if err != nil {
         return err
     }
     if len(configs) == 0 {
         clog.Info("no scheduled jobs found")
         return nil
     }
     for _, cfg := range configs {
         if cfg.ConfigFile != configFile {
             clog.Debugf("skipping job %s/%s from configuration file %s", cfg.ProfileName, cfg.CommandName, cfg.ConfigFile)
             continue
         }
         job := schedule.NewJob(handler, &cfg)
         err := job.Status()
         if err != nil {
-            clog.Error(err)
+            errors = append(errors, fmt.Errorf("failed to get status for job %s/%s: %w", cfg.ProfileName, cfg.CommandName, err))
         }
     }
     err = handler.DisplayStatus(profileName)
     if err != nil {
-        clog.Error(err)
+        errors = append(errors, fmt.Errorf("failed to display status: %w", err))
     }
+    if len(errors) > 0 {
+        return fmt.Errorf("failed to get status for some jobs: %v", errors)
+    }
     return nil
 }
schedule/handler_darwin_test.go (1)

185-251: Consider adding negative test cases.

The test covers successful scenarios well, but could benefit from additional test cases that verify error handling:

  1. Invalid configuration file paths
  2. Permission denied scenarios
  3. Network-related failures
 func TestReadingLaunchdScheduled(t *testing.T) {
     // ... existing test cases ...
+    errorTestCases := []struct {
+        name            string
+        job             Config
+        schedules       []*calendar.Event
+        expectedError   string
+    }{
+        {
+            name: "invalid_config_file",
+            job: Config{
+                ProfileName: "invalid",
+                ConfigFile: "/nonexistent/path/config.yaml",
+            },
+            expectedError: "no such file or directory",
+        },
+        {
+            name: "permission_denied",
+            job: Config{
+                ProfileName: "noperm",
+                ConfigFile: "/root/config.yaml",
+            },
+            expectedError: "permission denied",
+        },
+    }
+
+    for _, tc := range errorTestCases {
+        t.Run(tc.name, func(t *testing.T) {
+            handler := NewHandler(SchedulerLaunchd{}).(*HandlerLaunchd)
+            handler.fs = afero.NewMemMapFs()
+
+            _, err := handler.createPlistFile(
+                handler.getLaunchdJob(&tc.job, tc.schedules),
+                tc.job.Permission,
+            )
+            assert.ErrorContains(t, err, tc.expectedError)
+        })
+    }
 }
schedule_jobs_test.go (1)

203-270: Consider adding test cases for concurrent job removal.

The TestRemoveScheduledJobs function could benefit from additional test cases that verify behaviour when multiple jobs are being removed concurrently.

 func TestRemoveScheduledJobs(t *testing.T) {
     // ... existing test cases ...
+    t.Run("concurrent_removal", func(t *testing.T) {
+        handler := mocks.NewHandler(t)
+        handler.EXPECT().Init().Return(nil)
+        handler.EXPECT().Close()
+
+        // Create multiple jobs that will be removed concurrently
+        configs := []schedule.Config{}
+        for i := 0; i < 10; i++ {
+            configs = append(configs, schedule.Config{
+                ProfileName: fmt.Sprintf("profile_%d", i),
+                CommandName: "backup",
+                ConfigFile:  "configFile",
+                Permission:  "user",
+            })
+        }
+
+        handler.EXPECT().Scheduled("").Return(configs, nil)
+
+        // Expect removal of all jobs
+        for _, cfg := range configs {
+            handler.EXPECT().RemoveJob(&cfg, "user").Return(nil)
+        }
+
+        err := removeScheduledJobs(handler, "configFile", "")
+        assert.NoError(t, err)
+    })
 }
commands_schedule.go (1)

120-156: Simplify legacy flag handling in statusSchedule.

The nested if conditions make the code harder to follow. Consider restructuring for better readability.

-    if slices.Contains(flags.resticArgs, "--legacy") { // TODO: remove this option in the future
-        // single profile or group
-        if !slices.Contains(args, "--all") {
+    if slices.Contains(flags.resticArgs, "--legacy") {
+        clog.Warning("The --legacy flag is deprecated and will be removed in version 2.0.0")
+        
+        if slices.Contains(args, "--all") {
+            return handleAllProfilesStatus(c, flags, args)
+        }
+        return handleSingleProfileStatus(c, flags)
+    }

Then extract the logic into separate functions:

func handleSingleProfileStatus(c *config.Config, flags commandLineFlags) error {
    schedulerConfig, schedules, _, err := getScheduleJobs(c, flags)
    if err != nil {
        return err
    }
    if len(schedules) == 0 {
        clog.Warningf("profile or group %s has no schedule", flags.name)
        return nil
    }
    return statusScheduleProfileOrGroup(schedulerConfig, schedules, flags)
}

func handleAllProfilesStatus(c *config.Config, flags commandLineFlags, args []string) error {
    for _, profileName := range selectProfilesAndGroups(c, flags, args) {
        profileFlags := flagsForProfile(flags, profileName)
        scheduler, schedules, schedulable, err := getScheduleJobs(c, profileFlags)
        if err != nil {
            return err
        }
        if len(schedules) == 0 {
            continue
        }
        clog.Infof("%s %q:", cases.Title(language.English).String(schedulable.Kind()), profileName)
        if err := statusScheduleProfileOrGroup(scheduler, schedules, profileFlags); err != nil {
            clog.Error(err)
        }
    }
    return nil
}
schedule/handler_darwin.go (2)

41-42: Consider documenting the namePrefix constant.

Adding a comment explaining the purpose and format of the namePrefix would improve code clarity.

+// namePrefix is the prefix used for all launchd job labels managed by resticprofile
 const namePrefix = "local.resticprofile."

209-223: Consider implementing job retrieval error handling.

The Scheduled method could benefit from more robust error handling for file system operations.

 func (h *HandlerLaunchd) Scheduled(profileName string) ([]Config, error) {
+    if h.fs == nil {
+        return nil, fmt.Errorf("file system not initialized")
+    }
     jobs := make([]Config, 0)
     if profileName == "" {
         profileName = "*"
     } else {
         profileName = strings.ToLower(profileName)
     }
     // system jobs
     systemJobs := h.getScheduledJob(profileName, constants.SchedulePermissionSystem)
     jobs = append(jobs, systemJobs...)
     // user jobs
     userJobs := h.getScheduledJob(profileName, constants.SchedulePermissionUser)
     jobs = append(jobs, userJobs...)
     return jobs, nil
 }
examples/v2.yaml (1)

42-42: Remove trailing spaces.

There are trailing spaces on lines 42 and 206 that should be removed.

Apply this diff to remove the trailing spaces:

-    
+
-      
+

Also applies to: 206-206

🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 42-42: trailing spaces

(trailing-spaces)

Makefile (2)

303-308: Enhanced Lint Target with Cross-Platform Testing

The updated lint target now depends on $(GOBIN)/golangci-lint and executes linting for Darwin, Linux, and Windows by setting the GOOS variable. This approach helps catch platform-specific issues. It would be beneficial to add a brief comment explaining why linting under multiple OS environments is required.


310-317: Robust Fix Target Incorporating Automated Code Fixes

The revised fix target correctly depends on the golangci-lint installation and performs go mod tidy and go fix before running lint fixes across different GOOS settings. This order ensures that code dependencies are well maintained before applying automatic fixes. Verify that running lint fixes under multiple OS environments does not introduce unintended discrepancies.

crond/io.go (1)

69-69: Remember to handle character sets
A TODO mentions charsets; consider implementing it soon or clarifying encoding expectations.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2297a6 and e42c536.

📒 Files selected for processing (57)
  • Makefile (2 hunks)
  • calendar/event.go (1 hunks)
  • calendar/event_test.go (1 hunks)
  • codecov.yml (1 hunks)
  • commands.go (4 hunks)
  • commands_schedule.go (5 hunks)
  • commands_schedule_test.go (2 hunks)
  • commands_test.go (2 hunks)
  • complete_test.go (2 hunks)
  • config/config.go (1 hunks)
  • config/load_options.go (1 hunks)
  • crond/crontab.go (8 hunks)
  • crond/crontab_test.go (8 hunks)
  • crond/entry.go (1 hunks)
  • crond/io.go (3 hunks)
  • crond/parse_event.go (1 hunks)
  • crond/parse_event_test.go (1 hunks)
  • examples/linux.yaml (2 hunks)
  • examples/v2.yaml (10 hunks)
  • schedule/calendar_interval.go (1 hunks)
  • schedule/calendar_interval_test.go (1 hunks)
  • schedule/command_arguments.go (3 hunks)
  • schedule/command_arguments_test.go (1 hunks)
  • schedule/config.go (1 hunks)
  • schedule/config_test.go (1 hunks)
  • schedule/errors.go (1 hunks)
  • schedule/handler.go (1 hunks)
  • schedule/handler_crond.go (5 hunks)
  • schedule/handler_crond_test.go (1 hunks)
  • schedule/handler_darwin.go (7 hunks)
  • schedule/handler_darwin_test.go (2 hunks)
  • schedule/handler_fake_test.go (0 hunks)
  • schedule/handler_systemd.go (10 hunks)
  • schedule/handler_systemd_test.go (1 hunks)
  • schedule/handler_windows.go (4 hunks)
  • schedule/job.go (3 hunks)
  • schedule/job_test.go (1 hunks)
  • schedule/mocks/Handler.go (4 hunks)
  • schedule/removeonly_test.go (2 hunks)
  • schedule/schedule_test.go (1 hunks)
  • schedule/scheduler.go (0 hunks)
  • schedule/schedules.go (3 hunks)
  • schedule/schedules_test.go (2 hunks)
  • schedule/spaced_title.go (1 hunks)
  • schedule/spaced_title_test.go (1 hunks)
  • schedule/systemd_unit.go (1 hunks)
  • schedule/tree_darwin.go (1 hunks)
  • schedule_jobs.go (8 hunks)
  • schedule_jobs_test.go (12 hunks)
  • schtasks/taskscheduler.go (2 hunks)
  • schtasks/taskscheduler_test.go (3 hunks)
  • shell/split_arguments.go (1 hunks)
  • shell/split_arguments_test.go (1 hunks)
  • systemd/drop_ins.go (2 hunks)
  • systemd/generate.go (2 hunks)
  • systemd/read.go (1 hunks)
  • systemd/read_test.go (1 hunks)
💤 Files with no reviewable changes (2)
  • schedule/scheduler.go
  • schedule/handler_fake_test.go
✅ Files skipped from review due to trivial changes (2)
  • systemd/generate.go
  • schedule/tree_darwin.go
🧰 Additional context used
🪛 YAMLlint (1.35.1)
examples/linux.yaml

[warning] 111-111: wrong indentation: expected 12 but found 10

(indentation)

examples/v2.yaml

[error] 42-42: trailing spaces

(trailing-spaces)


[error] 206-206: trailing spaces

(trailing-spaces)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build and test (1.23, windows-latest)
🔇 Additional comments (99)
schedule/config_test.go (1)

24-24: Update test to align with struct changes

The test initialises the removeOnly field which may need to be updated based on the struct changes.

Please ensure this test is updated to align with any changes to the Config struct.

schedule/spaced_title.go (2)

7-18: Well-implemented string manipulation logic.

The implementation is efficient and handles edge cases well:

  • Uses strings.Builder for optimal performance
  • Correctly handles the first character
  • Avoids double spaces
  • Maintains original casing

1-1: Query: Is the darwin build tag necessary?

The function performs string manipulation without any OS-specific functionality. Consider whether this build constraint is required, as it limits the function's availability to macOS systems only.

✅ Verification successful

Darwin build tag in spaced_title.go appears unnecessary.
Analysis of the codebase shows that although several files use a darwin build tag, the implementation in schedule/spaced_title.go is confined to generic string manipulation and does not rely on any darwin‐specific functionality. Consider removing the build tag to improve portability.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any darwin-specific functionality in the codebase that might justify this constraint
rg -l --type go '//go:build.*darwin' | xargs rg -l 'darwin|macos|cocoa'

Length of output: 605

schedule/spaced_title_test.go (1)

7-25: Well-structured test implementation.

The test implementation is clean and effective:

  • Table-driven tests for easy maintenance
  • Clear test cases with descriptive inputs
  • Informative error messages
  • Good variety of basic scenarios
schedule/handler_systemd.go (1)

12-12: Verify Go version compatibility for “slices”
Make sure the project targets a Go version that supports the standard library “slices” package (introduced in Go 1.21).

config/load_options.go (1)

3-7: Looks good
This closure cleanly sets the config file in the Config struct, improving flexibility.

schedule/errors.go (2)

7-7: LGTM!

The error variable is well-defined with a clear and concise message, following Go's error naming convention.


8-8: LGTM!

The error variable is well-defined with a clear and concise message, following Go's error naming convention.

schedule/systemd_unit.go (1)

1-11: LGTM!

The struct is well-defined with clear field names and appropriate JSON tags. The build constraint correctly excludes unsupported platforms.

schedule/removeonly_test.go (2)

7-7: LGTM! Good addition of the require package.

Using require package from testify enhances error handling by failing fast when assertions fail.


43-48: LGTM! Improved test setup and error handling.

The changes improve the test setup by:

  1. Using require.NoError for handler initialization, which fails fast if initialization fails
  2. Directly creating the job without using a scheduler, which simplifies the test
schedule/command_arguments.go (2)

3-6: LGTM! Clean import organisation.

The addition of the slices package is appropriate for the new functionality.


18-28: LGTM! Well-implemented Trim method.

The method efficiently creates a new CommandArguments instance with specified flags removed. Good use of capacity pre-allocation and the slices.Contains function.

schedule/handler.go (1)

15-15: LGTM! Enhanced Handler interface.

The changes improve the interface by:

  1. Adding profile parameter to DisplaySchedules for better context
  2. Adding Scheduled method to support reading existing scheduled configuration

These changes align well with the PR objectives for scheduling improvements.

Also applies to: 20-20

schedule/schedule_test.go (1)

34-37: LGTM! Consistent test refactoring.

The changes improve test structure by:

  1. Using handler directly instead of scheduler
  2. Maintaining consistent initialization and cleanup pattern across all test functions
  3. Preserving platform-specific test skipping logic

This refactoring aligns with the broader changes in the codebase for better scheduling management.

Also applies to: 44-47, 54-57, 63-66, 72-75

crond/parse_event.go (1)

41-86: LGTM! Well-structured parsing logic.

The parseField function is well-structured with clear separation between wildcard, list, and range parsing. The error handling is thorough and the code is easy to follow.

schedule/handler_crond_test.go (1)

67-70: Verify the content of the crontab file.

The test verifies that jobs are scheduled but doesn't validate the actual content written to the crontab file.

Add verification of the crontab file content:

 	scheduled, err := handler.Scheduled("")
 	require.NoError(t, err)
 
 	assert.ElementsMatch(t, expectedJobs, scheduled)
+
+	// Verify crontab file content
+	content, err := afero.ReadFile(handler.fs, tempFile)
+	require.NoError(t, err)
+	assert.Contains(t, string(content), "0 * * * *")
schedule/job.go (1)

44-56: LGTM! Well-structured error handling.

The error handling in the Create method is well-structured, with clear separation between displaying schedules, parsing schedules, and creating the job.

schedule/schedules.go (1)

15-25: Well-structured header formatting function!

The new displayHeader function effectively encapsulates the header formatting logic, making it reusable across different display functions. The conditional index display for multiple schedules is a nice touch.

schedule/job_test.go (2)

1-12: Excellent test structure improvements!

The changes to use the schedule_test package and mock handler improve test isolation and maintainability. The switch to require ensures better error handling in tests.


14-29: Well-structured test cases with clear error scenarios.

The test cases effectively cover both happy and error paths. The mock expectations are clear and the error scenarios are well-defined.

Also applies to: 31-44, 46-58, 60-75

schedule/handler_systemd_test.go (1)

1-14: Good use of build constraints and imports!

The build constraints and package structure are well-organised. The imports are properly grouped and the test dependencies are appropriate.

commands_schedule_test.go (2)

68-71: LGTM: Global section handling improvement.

Good addition of global section retrieval and validation. This ensures the test has access to the scheduler configuration.

Also applies to: 99-99


49-53: Verify crontab entry format.

The crontab entry appears to be hardcoded with specific paths ("/workdir", "/home/resticprofile"). This might cause issues if the paths differ in the test environment.

schedule/schedules_test.go (2)

17-17: LGTM: Improved error handling with require.

Good improvement in error handling by replacing assert with require. This ensures tests fail fast when errors occur.

Also applies to: 22-22, 27-27


91-92: LGTM: Improved platform checks.

Good improvement in platform detection by using the dedicated platform package functions instead of direct GOOS checks.

schedule/handler_windows.go (2)

34-42: LGTM: Improved schedule display handling.

Good refactoring of the DisplaySchedules method to include parsing logic and profile information.


78-78: LGTM: More specific error types.

Good improvement in error handling by using the more specific ErrScheduledJobNotFound instead of ErrServiceNotFound.

Also applies to: 90-90

systemd/read_test.go (1)

1-2: LGTM! Build constraints are correctly placed.

The build constraints ensure the tests only run on supported platforms.

schedule/handler_crond.go (2)

122-155: LGTM! Well-structured job retrieval logic.

The new Scheduled method effectively retrieves and organizes scheduled jobs, with proper error handling and efficient use of slices.


111-113: Verify impact of error handling change.

The error returned has been changed from ErrServiceNotFound to ErrScheduledJobNotFound. Ensure this change is consistent across the codebase.

✅ Verification successful

Impact of error handling change verified

The change is consistent throughout the codebase. No instances of ErrServiceNotFound were found, while ErrScheduledJobNotFound is used in all relevant locations (e.g. in handler_crond.go, handler_windows.go, handler_systemd.go, etc.).

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for uses of both error types
echo "Searching for ErrServiceNotFound usage:"
rg "ErrServiceNotFound"
echo -e "\nSearching for ErrScheduledJobNotFound usage:"
rg "ErrScheduledJobNotFound"

Length of output: 1157

schedule_jobs.go (1)

13-57: LGTM: The scheduleJobs function has been simplified.

The removal of the profileName parameter and the direct initialisation of the handler improve the function's clarity and reduce unnecessary parameters.

calendar/event_test.go (1)

164-165: Well-structured test improvements!

The changes enhance test precision by:

  1. Adding refNoSecond to test exact-second scenarios
  2. Making test data more flexible with per-test reference times
  3. Properly testing second-level precision in scheduling

Also applies to: 167-170, 171-180

schedule/handler_darwin.go (1)

11-12: Good package organization!

The addition of slices and clog packages improves code maintainability and logging capabilities.

Also applies to: 16-17

complete_test.go (1)

274-275: Comprehensive test coverage for the new --start flag!

The test cases properly verify command completion behaviour with the new --start flag across various scenarios.

Also applies to: 281-282, 298-300

commands.go (3)

29-30: Good use of sync.Once for elevation handling!

Using sync.Once ensures that elevation is attempted only once, preventing potential issues with multiple elevation attempts.


104-106: Clear and complementary flag descriptions.

The --start and --no-start flags are well-documented and provide clear, complementary functionality.


323-340: Improved elevation retry logic.

The use of sync.Once in retryElevated ensures a single elevation attempt, making the code more robust.

Run this script to verify error handling in elevation scenarios:

✅ Verification successful

Verification of Elevation Retry Error Handling

The output confirms that the error handling and elevation retry logic, including checking for "denied" errors and using elevation.Do (which internally ensures a single elevation attempt), is implemented as expected.

  • Consistent error handling is present in the elevation block.
  • The use of the elevation.Do function properly restricts the elevation retry to one attempt as intended.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent error handling in elevation-related code

# Look for error handling patterns in elevation code
rg -A 5 'elevation.*Do\(' 
rg -A 5 'strings\.Contains.*denied'

Length of output: 611

schedule/mocks/Handler.go (2)

151-167: LGTM! Method signature updated to include profile parameter.

The DisplaySchedules method signature has been updated to include the profile parameter, which aligns with the PR's objective of enhancing scheduling capabilities.


395-451: LGTM! New method added to support reading back scheduled configuration.

The Scheduled method has been added to support retrieving scheduled jobs for a given profile, which directly implements the PR's objective of reading back existing scheduled configuration.

commands_test.go (2)

17-17: LGTM! Added require package for better test assertions.

Using require package from testify improves test reliability by failing fast when assertions fail.


70-72: LGTM! Refactored to use handler instead of scheduler.

The test has been updated to use the new handler interface, which aligns with the changes in the scheduling system.

schtasks/taskscheduler_test.go (2)

117-118: LGTM! Enhanced assertion messages for better test diagnostics.

Added descriptive messages to assertions to make test failures more understandable.


396-445: LGTM! Added comprehensive test for task registration.

The new test validates the task registration functionality by:

  1. Creating multiple tasks with different configurations
  2. Verifying successful registration
  3. Cleaning up tasks after test completion
schtasks/taskscheduler.go (2)

522-524: LGTM! Improved error handling for non-existent tasks.

Enhanced error handling to provide clearer feedback when attempting to delete non-existent tasks.


565-602: LGTM! Added function to retrieve registered tasks.

The new Registered function:

  1. Retrieves all tasks from the scheduler
  2. Filters tasks by prefix
  3. Parses task paths to extract profile and command names
  4. Constructs Config objects with task details

This implementation directly supports the PR's objective of reading back existing scheduled configuration.

config/config.go (1)

134-138: LGTM! Well-designed enhancement to the Load function.

The addition of variadic options provides a flexible way to customise the configuration object during initialisation.

codecov.yml (1)

14-14: LGTM! Sensible exclusion of mock files from coverage reporting.

The glob pattern **/mocks/*.go correctly excludes all mock files from any directory in the codebase.

examples/linux.yaml (1)

15-15: LGTM! Global scheduler configuration added.

The scheduler configuration correctly specifies crontab as the scheduler with the path /tmp/crontab.

examples/v2.yaml (2)

10-21: LGTM! Enhanced group configuration with error handling and scheduling.

The changes correctly add:

  • Global scheduler configuration
  • Error handling with continue-on-error
  • Schedule configuration for the group

103-244: LGTM! Comprehensive profile enhancements.

The changes successfully add:

  • Extended error handling with run-after-fail
  • Scheduled operations for various tasks
  • Monitoring capabilities with Prometheus integration
  • Proper handling of special characters in paths
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 206-206: trailing spaces

(trailing-spaces)

Makefile (1)

90-93: New golangci-lint Target Integration is Well Designed

The addition of the target
$(GOBIN)/golangci-lint: verify $(GOBIN)/eget
ensures that golangci-lint is installed via eget prior to use. This is consistent with other targets in the Makefile. Consider periodically verifying that the pinned version (v1.61.0) remains current with the linter’s releases.

crond/io.go (9)

11-11: No meaningful change identified


12-12: Correct import for filesystem abstraction


20-22: Clear shared error constant
This uniform error message eases error handling consistently across the codebase.


24-48: Robust crontab loading logic
This code neatly accounts for an empty file scenario vs. the absence of a crontab. Error handling is thorough and readable.


50-60: Efficient approach to saving crontab data
The use of piping for the crontab binary or writing to a file is well-structured and covers both scenarios well.


73-75: Good use of afero.File
Opening with afero keeps the code flexible for testing on different filesystems.


87-89: Graceful handling of missing file
Setting the error to nil if the file does not exist is appropriate, offering a clean “empty” fallback.


94-94: saveCrontabFile signature
Straightforward function signature, matching the rest of the afero pattern.


105-105: Sensible write permissions
Using defaultCrontabFilePerms with afero.WriteFile aligns with the overall design.

crond/crontab.go (19)

4-4: Importing 'errors'
No concerns here; continuing to use the standard library is sensible.


10-10: No meaningful change identified


11-11: Integral afero import
This import is required for the new filesystem abstraction.


25-28: Regex compilation
These patterns rely on the corrected expressions above to compile and match successfully.


31-31: Distinct error for unmatched entries
Centralising this error helps handle unexpected lines gracefully.


37-37: Filesystem field
Storing an afero.Fs in the struct enables flexible reading/writing of the crontab within tests.


40-44: Initialising with an OS filesystem
Offers a clear default while still allowing overriding with a memory filesystem.


56-56: Fluent method returning the same Crontab
Permits chaining calls neatly, improving code readability.


62-65: File-based approach
Allows toggling between crontab binary usage and direct file handling.


67-72: SetFs for custom filesystem
Facilitates simpler tests and better mock coverage.


100-100: Delegating generation
Invoking c.generate encapsulates entry creation cleanly.


146-157: New generate function
Separates concerns of generating entries vs. orchestrating a crontab rewrite, improving clarity.


160-160: Using loadCrontab
Consistent with the new load/save pattern, ensuring code reuse.


205-205: saveCrontab invocation
Keeps logic consistent for rewriting the crontab, whether it's a file or binary approach.


217-217: Same saving flow for removal
Employing the same function for final saving is a good approach.


222-235: Fetching existing entries
Exposes a helpful public API to retrieve the existing entries from a crontab section.


325-340: Splitting lines to parse
Nicely filters out comments and empty lines, then delegates to parseEntry.


342-386: parseEntry handles legacy and new schedules
Thorough approach covering different command patterns.


388-394: Retrieving user and workdir
Trimming spaces and used substrings is clear and avoids extraneous whitespace.

crond/crontab_test.go (19)

14-14: Importing afero
Supports memory-based tests, removing dependency on real file I/O.


22-22: generate invoked in test
This verifies that producing crontab lines works without errors.


33-33: Basic generation test
Quickly checks correct lines are written.


44-44: Work directory test
Confirms correct prefix with 'cd workdir &&'.


80-87: Additional test data
These test lines help confirm that lines beginning with '#' are ignored and that user commands can contain cd /workdir.


182-182: Error check for missing crontab file
Verifies logic to handle a file not set scenario.


184-184: Using MemMapFs
Permits in-memory testing consistently.


185-185: SetFs usage
Ensures the Crontab uses our mock filesystem instead of disk.


188-191: Verifying file presence
Correctly checks whether the file was created.


193-195: Re-checking file presence
Confirms the file stays consistent after rewriting.


205-205: Setting manual crontab.user
Tests behaviour when overriding the user.


213-214: Initialising test path
Sets up a realistic spool directory for testing.


217-217: Writing a userLine
Ensures we preserve existing lines outside the resticprofile section.


225-225: SetFs again
Maintains test isolation by specifying an in-memory filesystem.


266-266: Preventing changes to ‘DO NOT EDIT’ file
Rightly raises an error if the system states the file must not be edited.


309-309: Mock binary
Builds a crontab mock to test fallback logic.


366-366: TestParseEntry introduced
Exercises a wide variety of crontab line formats thoroughly.


367-431: Comprehensive coverage of parseEntry
Demonstrates behaviour with user columns, workdir usage, and quoted config files.


433-473: TestGetEntries
Checks retrieval of the resticprofile section from a crontab, including empty or absent sections.

schedule/calendar_interval.go Outdated Show resolved Hide resolved
schedule/calendar_interval.go Show resolved Hide resolved
schedule/handler_systemd.go Outdated Show resolved Hide resolved
crond/parse_event.go Outdated Show resolved Hide resolved
schedule/job.go Outdated Show resolved Hide resolved
schedule/handler_windows.go Show resolved Hide resolved
schedule/handler_crond.go Show resolved Hide resolved
commands_schedule.go Outdated Show resolved Hide resolved
examples/linux.yaml Outdated Show resolved Hide resolved
crond/crontab.go Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (18)
main_test.go (1)

23-54: Consider enhancing error handling and cleanup.

The error handling and cleanup could be more robust:

  1. Use defer for cleanup to ensure it runs even if tests panic
  2. Make error handling consistent across all error cases

Consider this improvement:

 func TestMain(m *testing.M) {
 	tempDir, err := os.MkdirTemp("", "resticprofile")
 	if err != nil {
 		fmt.Printf("cannot create temp dir: %v", err)
 		os.Exit(1)
 	}
+	// Ensure cleanup runs even if tests panic
+	defer os.RemoveAll(tempDir)
+
 	mockBinary = filepath.Join(tempDir, "shellmock")
 	if platform.IsWindows() {
 		mockBinary += ".exe"
 	}
 	cmdMock := exec.Command("go", "build", "-buildvcs=false", "-o", mockBinary, "./shell/mock")
 	if output, err := cmdMock.CombinedOutput(); err != nil {
-		fmt.Fprintf(os.Stderr, "Error building shell/mock binary: %s\nCommand output: %s\n", err, string(output))
+		fmt.Printf("Error building shell/mock binary: %s\nCommand output: %s\n", err, string(output))
 		os.Exit(1)
 	}

 	echoBinary = filepath.Join(tempDir, "shellecho")
 	if platform.IsWindows() {
 		echoBinary += ".exe"
 	}
 	cmdEcho := exec.Command("go", "build", "-buildvcs=false", "-o", echoBinary, "./shell/echo")
 	if output, err := cmdEcho.CombinedOutput(); err != nil {
-		fmt.Fprintf(os.Stderr, "Error building shell/echo binary: %s\nCommand output: %s\n", err, string(output))
+		fmt.Printf("Error building shell/echo binary: %s\nCommand output: %s\n", err, string(output))
 		os.Exit(1)
 	}

 	exitCode := m.Run()
-	_ = os.Remove(mockBinary)
-	_ = os.Remove(echoBinary)
-	_ = os.RemoveAll(tempDir)
 	os.Exit(exitCode)
 }
crond/parse_event_test.go (3)

17-18: Remove duplicate test case.

The test case for "00 00 01 * 3" appears twice consecutively.

 		{"00 00 01 * 3", "Wed *-*-01 00:00:00"},
-		{"00 00 01 * 3", "Wed *-*-01 00:00:00"},

15-38: Consider adding comments to explain test scenarios.

The test cases would benefit from comments explaining the specific scenarios being tested, particularly for complex cron expressions. This would make it easier for other developers to understand the test coverage and add new test cases.

Example of how to improve the documentation:

 	}{
+		// Test all weekdays except Friday
 		{"00 00 * * 1,2,3,4,6,0", "Sun..Thu,Sat *-*-* 00:00:00"},
+		// Test multiple hours on specific days
 		{"23 01,02 * * 1,0", "Sun,Mon *-*-* 01,02:23:00"},

50-78: Enhance invalid test cases coverage.

Whilst the current test cases cover basic invalid inputs, consider adding:

  1. Tests for boundary values (e.g., "60 * * * *" for invalid minutes)
  2. Tests for invalid month/day combinations
  3. Comments explaining why each input is invalid

Additionally, the multiple space test cases (lines 54-63) could be consolidated into a single parameterised test.

Example of how to improve the test cases:

 	}{
+		// Empty string
 		{""},
+		// Various lengths of whitespace (could be parameterised)
 		{" "},
 		{"  "},
-		{"   "},
-		{"    "},
-		{"     "},
-		{"      "},
-		{"       "},
-		{"        "},
-		{"         "},
+		// Invalid minute value (> 59)
 		{"99 00 * * 0,1"},
+		// Invalid syntax: dash without number
 		{"0- 00 * * 0,1"},
+		// Invalid syntax: dash at start
 		{"-0 00 * * 0,1"},
+		// Invalid syntax: comma without number
 		{"0, 00 * * 0,1"},
+		// Invalid syntax: comma at start
 		{",0 00 * * 0,1"},
+		// Completely invalid format
 		{"invalid"},
+		// Invalid month/day combination
+		{"00 00 31 2 *"},
+		// Invalid hour value (> 23)
+		{"00 24 * * *"},
 	}
schedule/job.go (3)

50-60: Consider wrapping errors for better context.

While the error handling is correct, consider wrapping errors to provide more context about where they occurred. This would make debugging easier.

Apply this diff to wrap errors:

-	if err := j.handler.DisplaySchedules(j.config.ProfileName, j.config.CommandName, j.config.Schedules); err != nil {
+	if err := j.handler.DisplaySchedules(j.config.ProfileName, j.config.CommandName, j.config.Schedules); err != nil {
+		return fmt.Errorf("failed to display schedules: %w", err)
-		return err
 	}

-	schedules, err := j.handler.ParseSchedules(j.config.Schedules)
-	if err != nil {
-		return err
+	schedules, err := j.handler.ParseSchedules(j.config.Schedules)
+	if err != nil {
+		return fmt.Errorf("failed to parse schedules: %w", err)
 	}

-	if err = j.handler.CreateJob(j.config, schedules, permission); err != nil {
-		return err
+	if err = j.handler.CreateJob(j.config, schedules, permission); err != nil {
+		return fmt.Errorf("failed to create job: %w", err)
 	}

Don't forget to add the import:

 import (
 	"errors"
+	"fmt"
 )

93-95: Consider wrapping errors for better context.

Similar to the Create method, consider wrapping errors to provide more context about where they occurred.

Apply this diff to wrap errors:

-	if err := j.handler.DisplaySchedules(j.config.ProfileName, j.config.CommandName, j.config.Schedules); err != nil {
-		return err
+	if err := j.handler.DisplaySchedules(j.config.ProfileName, j.config.CommandName, j.config.Schedules); err != nil {
+		return fmt.Errorf("failed to display schedules: %w", err)
 	}

97-99: Consider wrapping errors for better context.

Similar to previous segments, consider wrapping errors to provide more context about where they occurred.

Apply this diff to wrap errors:

-	if err := j.handler.DisplayJobStatus(j.config); err != nil {
-		return err
+	if err := j.handler.DisplayJobStatus(j.config); err != nil {
+		return fmt.Errorf("failed to display job status: %w", err)
 	}
schedule/spaced_title.go (1)

1-2: Consider documenting the build constraint.

The build constraint indicates this file is only used on Darwin systems. Consider adding a comment explaining why this is Darwin-specific.

schedule/command_arguments.go (1)

57-74: Consider optimising ConfigFile method.

The method could be more efficient by:

  1. Using a single pass through the arguments
  2. Avoiding repeated string operations

Consider this optimised implementation:

 func (ca CommandArguments) ConfigFile() string {
-	if len(ca.args) == 0 {
-		return ""
-	}
-	var lastConfig string
-	for i, arg := range ca.args {
-		if arg == "--config" {
-			if i+1 < len(ca.args) {
-				lastConfig = ca.args[i+1]
-			}
-		} else if strings.HasPrefix(arg, "--config=") {
-			lastConfig = strings.TrimPrefix(arg, "--config=")
-		}
-	}
-	return lastConfig
+	const configFlag = "--config"
+	const configPrefix = configFlag + "="
+	
+	var lastConfig string
+	for i := len(ca.args) - 1; i >= 0; i-- {
+		arg := ca.args[i]
+		if arg == configFlag && i+1 < len(ca.args) {
+			return ca.args[i+1]
+		}
+		if strings.HasPrefix(arg, configPrefix) {
+			return arg[len(configPrefix):]
+		}
+	}
+	return ""
 }
commands_schedule.go (1)

118-174: Consider consolidating legacy and new code paths.

The statusSchedule function has significant code duplication between legacy and new implementations. Consider refactoring to reduce duplication.

shell/command_test.go (1)

28-34: LGTM! Good improvements to test setup.

The changes improve the test setup by using a temporary directory and proper path construction. This makes the tests more reliable and prevents potential conflicts.

Consider adding a newline to the error message for better readability:

-		fmt.Printf("cannot create temp dir: %v", err)
+		fmt.Printf("cannot create temp dir: %v\n", err)
schedule_jobs.go (3)

13-57: Consider improving error handling and resource management.

Two suggestions for improvement:

  1. Move defer handler.Close() after the error check to avoid calling Close on a failed initialization.
  2. Add more context to error messages for better debugging.

Apply this diff:

 func scheduleJobs(handler schedule.Handler, configs []*config.Schedule) error {
     wd, err := os.Getwd()
     if err != nil {
-        return err
+        return fmt.Errorf("failed to get working directory: %w", err)
     }
     binary, err := os.Executable()
     if err != nil {
-        return err
+        return fmt.Errorf("failed to get executable path: %w", err)
     }
 
     err = handler.Init()
     if err != nil {
-        return err
+        return fmt.Errorf("failed to initialize handler: %w", err)
     }
     defer handler.Close()

59-94: Enhance error handling in removeJobs function.

Consider adding more context to error messages and improving error handling robustness.

Apply this diff:

 func removeJobs(handler schedule.Handler, configs []*config.Schedule) error {
     err := handler.Init()
     if err != nil {
-        return err
+        return fmt.Errorf("failed to initialize handler: %w", err)
     }
     defer handler.Close()

132-165: Improve error handling for DisplayStatus.

Consider propagating the DisplayStatus error instead of just logging it.

Apply this diff:

     err = handler.DisplayStatus(profileName)
     if err != nil {
-        clog.Error(err)
+        return fmt.Errorf("failed to display status: %w", err)
     }
-    return nil
+    return err
schedule_jobs_test.go (1)

203-272: Consider adding more edge cases to the test suite.

The table-driven tests are well-structured, but consider adding these scenarios:

  1. Multiple jobs in the same config file
  2. Error cases for permission denied
systemd/read.go (3)

15-51: Add input validation and improve error handling.

The function could benefit from:

  1. Input validation for the unit name format
  2. More specific error messages for different failure scenarios
  3. Documentation explaining the expected format and behaviour

Consider applying this improvement:

+// Read parses systemd service and timer unit files and returns their configuration.
+// The unit parameter should be in the format "[email protected]".
+// The unitType parameter determines whether to read from system or user unit directories.
 func Read(unit string, unitType UnitType) (*Config, error) {
+    if !strings.HasSuffix(unit, ".service") {
+        return nil, fmt.Errorf("invalid unit name format: %s (must end with .service)", unit)
+    }
     var err error
     unitsDir := systemdSystemDir

113-121: Add input validation and error handling.

The function should validate the input format and return an error for invalid filenames.

Consider applying this improvement:

-func parseServiceFileName(filename string) (profileName, commandName string) {
+func parseServiceFileName(filename string) (profileName, commandName string, err error) {
+    if !strings.HasPrefix(filename, "resticprofile-") || !strings.HasSuffix(filename, ".service") {
+        return "", "", fmt.Errorf("invalid filename format: %s", filename)
+    }
     filename = strings.TrimPrefix(filename, "resticprofile-")
     filename = strings.TrimSuffix(filename, ".service")
-    commandName, profileName, _ = strings.Cut(filename, "@")
+    var found bool
+    commandName, profileName, found = strings.Cut(filename, "@")
+    if !found {
+        return "", "", fmt.Errorf("invalid filename format: missing @ separator")
+    }
     profileName = strings.TrimPrefix(profileName, "profile-")
     return
 }

123-130: Add documentation explaining priority mapping.

The function would benefit from documentation explaining the mapping between systemd priorities and schedule priorities.

Consider applying this improvement:

+// getPriority maps systemd CPU scheduling policy to schedule priority constants.
+// - "idle" maps to background priority
+// - all other values map to standard priority
 func getPriority(input string) string {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e42c536 and 8b41389.

📒 Files selected for processing (25)
  • Makefile (2 hunks)
  • calendar/event.go (1 hunks)
  • commands_schedule.go (6 hunks)
  • crond/crontab.go (8 hunks)
  • crond/entry.go (1 hunks)
  • crond/parse_event.go (1 hunks)
  • crond/parse_event_test.go (1 hunks)
  • examples/linux.yaml (4 hunks)
  • main_test.go (1 hunks)
  • schedule/calendar_interval.go (1 hunks)
  • schedule/command_arguments.go (3 hunks)
  • schedule/command_arguments_test.go (1 hunks)
  • schedule/handler_crond.go (6 hunks)
  • schedule/handler_darwin.go (7 hunks)
  • schedule/handler_systemd.go (10 hunks)
  • schedule/job.go (3 hunks)
  • schedule/spaced_title.go (1 hunks)
  • schedule/spaced_title_test.go (1 hunks)
  • schedule_jobs.go (8 hunks)
  • schedule_jobs_test.go (12 hunks)
  • shell/command_test.go (1 hunks)
  • shell/split_arguments.go (1 hunks)
  • shell/split_arguments_test.go (1 hunks)
  • systemd/read.go (1 hunks)
  • systemd/read_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • calendar/event.go
  • examples/linux.yaml
  • shell/split_arguments.go
  • schedule/spaced_title_test.go
  • shell/split_arguments_test.go
  • Makefile
  • systemd/read_test.go
  • crond/entry.go
  • crond/parse_event.go
  • schedule/calendar_interval.go
  • crond/crontab.go
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build and test (1.23, windows-latest)
🔇 Additional comments (40)
main_test.go (1)

24-28: Well done on implementing temporary directory for test binaries!

Good practice using os.MkdirTemp for test isolation. This makes the tests more reliable and portable across different environments.

crond/parse_event_test.go (1)

1-8: Proper package structure and testing imports.

The package declaration and imports are well-organised, using appropriate testing libraries.

schedule/job.go (2)

11-11: LGTM! Well-defined error variable.

The error variable follows Go's naming convention, and the message clearly explains the issue.


19-30: LGTM! Constructor with proper validation.

The constructor follows Go's naming convention and includes appropriate nil checks with descriptive panic messages.

schedule/spaced_title.go (1)

7-21: LGTM! Efficient implementation using strings.Builder.

The implementation correctly handles all edge cases and uses strings.Builder for optimal performance.

schedule/command_arguments.go (1)

18-28: LGTM! Efficient implementation of Trim method.

The implementation correctly pre-allocates capacity and uses slices.Contains for efficient filtering.

schedule/command_arguments_test.go (1)

61-87: LGTM! Comprehensive test coverage.

The test cases thoroughly cover:

  • Empty arguments
  • Missing config values
  • Multiple config flags
  • Various flag positions
  • Different config formats
commands_schedule.go (2)

18-20: Consider adding deprecation timeline.

The legacy flag warning message still lacks a clear deprecation timeline.


76-116: LGTM! Improved error handling in removeSchedule.

The function now:

  • Continues processing other jobs on error
  • Handles --all flag correctly
  • Uses more descriptive variable names
shell/command_test.go (1)

43-44: LGTM! Good cleanup handling.

The cleanup has been improved to remove both the binary and the temporary directory, ensuring no test artifacts are left behind.

schedule_jobs.go (3)

96-130: Well-implemented error handling and logging!

Good use of:

  • errors.Join for error aggregation
  • Proper debug logging
  • Clear error messages

167-206: Well-structured implementation with proper error handling!

Good use of:

  • errors.Join for error aggregation
  • Proper debug logging
  • Clear error messages

208-231: Clean and well-structured implementation!

Good handling of both scheduled and removal-only cases.

schedule_jobs_test.go (3)

21-202: Test modifications are thorough and maintain good coverage!

Good updates to reflect the API changes and error handling improvements.


274-299: Well-structured failure test!

Good coverage of error scenarios and proper error verification.


301-390: Comprehensive test coverage with good edge cases!

Well-structured tests with proper verification of both success and failure scenarios.

systemd/read.go (2)

93-102: LGTM!

The function is well-implemented with clear return semantics.


104-111: LGTM!

The function is well-implemented with clear return semantics.

schedule/handler_crond.go (4)

54-60: LGTM!

The function correctly parses and displays schedules, with proper error handling.


84-87: LGTM!

The method chaining improves readability whilst maintaining functionality.


106-109: LGTM!

The method chaining improves readability whilst maintaining functionality.


125-157: LGTM!

The function correctly retrieves and parses scheduled jobs, with proper error handling and schedule grouping.

schedule/handler_darwin.go (8)

89-96: LGTM!

The function correctly parses and displays schedules, with proper error handling.


122-133: LGTM!

The function correctly handles service creation and startup, with proper error handling.


225-258: LGTM!

The function correctly configures the launchd job with proper environment, priority, and logging settings.


260-278: LGTM!

The function correctly retrieves and processes scheduled jobs, with proper error handling and permission settings.


280-290: LGTM!

The function correctly generates glob patterns for both system and user paths, with proper error handling.


292-301: LGTM!

The function correctly parses filenames to extract command and profile information.


323-345: LGTM!

The function correctly handles plist file creation with proper permissions and encoding.


347-361: LGTM!

The function correctly reads and decodes plist files, with proper error handling.

schedule/handler_systemd.go (10)

74-77: LGTM!

The function correctly delegates schedule display to the appropriate helper function.


106-166: LGTM!

The function correctly handles systemd job creation and activation, with proper unit type and network dependency handling.


169-229: LGTM!

The function correctly handles systemd job removal and cleanup, with proper unit type handling and existence checks.


232-249: LGTM!

The function correctly displays job status information, with proper unit type handling and existence checks.


251-270: LGTM!

The function correctly retrieves both system and user jobs, with proper error handling.


338-352: LGTM!

The function correctly handles systemd configuration reload, with proper unit type handling and error management.


354-382: LGTM!

The function correctly lists systemd units, with proper JSON parsing and error handling.


384-392: LGTM!

The function correctly checks unit load status, with proper error handling.


394-415: LGTM!

The function correctly retrieves and processes unit configurations, with proper error handling.


417-444: LGTM!

The function correctly converts systemd configuration to schedule configuration, with proper field mapping and permission handling.

systemd/read.go Show resolved Hide resolved
systemd/read.go Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (1)
shell/command_test.go (1)

371-380:

🧩 Analysis chain

Consider investigating and fixing the Linux test issue.

The comment indicates an unresolved issue with TestInterruptShellCommand on Linux, leading to the test being skipped. This reduces test coverage on Linux systems.

Let's investigate if this is a known issue:

Would you like me to help investigate the Linux signal handling issue and propose a fix?


🏁 Script executed:

#!/bin/bash
# Search for related issues or discussions about Linux signal handling
rg -i "linux.*signal|signal.*linux" -g "!{vendor,dist}/*"

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Print the context around TestInterruptShellCommand to see comments and further details
rg -C 5 "func TestInterruptShellCommand" shell/command_test.go

Length of output: 331

Investigate and resolve the skipped Linux test in TestInterruptShellCommand.

The test is currently skipped on Linux due to known signal handling issues, which reduces our Linux test coverage. It would be advantageous to re-investigate the signal handling logic on Linux and update the test or underlying code accordingly.

🧹 Nitpick comments (5)
schedule/spaced_title.go (2)

7-10: Consider adding godoc examples.

The documentation is clear and comprehensive. Consider enhancing it with godoc examples to make it even more useful for developers.

Add examples in godoc format:

 // spacedTitle adds spaces before capital letters in a string, except for the first character
 // or when the capital letter follows a space. For example, "ThisIsATest" becomes "This Is A Test".
 // If the input is empty or contains no capital letters, it is returned unchanged.
 // This function is only used by the launchd handler on macOS.
+//
+// Example:
+//
+//	spacedTitle("ResticProfile") // Returns "Restic Profile"
+//	spacedTitle("ABC")          // Returns "A B C"
+//	spacedTitle("already spaced") // Returns "already spaced"

11-22: Consider minor improvements to the implementation.

The implementation is efficient and handles edge cases well. Consider these minor improvements:

 func spacedTitle(title string) string {
+	if title == "" {
+		return title
+	}
+	const (
+		upperA = 'A'
+		upperZ = 'Z'
+	)
 	var previous rune
 	sb := strings.Builder{}
 	for _, char := range title {
-		if char >= 'A' && char <= 'Z' && previous != ' ' && sb.Len() > 0 {
+		if char >= upperA && char <= upperZ && previous != ' ' && sb.Len() > 0 {
 			sb.WriteByte(' ')
 		}
 		sb.WriteRune(char)
 		previous = char
 	}
 	return sb.String()
 }
systemd/read.go (2)

68-68: Use a named constant for the initial sections map size.

The magic number 3 for the initial map size could be replaced with a named constant to improve code maintainability.

+const defaultSectionsMapSize = 3 // Unit, Service, and Timer sections
-	sections := make(map[string]map[string][]string, 3)
+	sections := make(map[string]map[string][]string, defaultSectionsMapSize)

128-134: Add error handling for malformed service filenames.

The function assumes a specific filename format but doesn't handle malformed inputs. Consider returning an error for invalid formats.

-func parseServiceFileName(filename string) (profileName, commandName string) {
+func parseServiceFileName(filename string) (profileName, commandName string, err error) {
 	filename = strings.TrimPrefix(filename, "resticprofile-")
+	if filename == "" {
+		return "", "", fmt.Errorf("invalid filename format: missing 'resticprofile-' prefix")
+	}
 	filename = strings.TrimSuffix(filename, ".service")
-	commandName, profileName, _ = strings.Cut(filename, "@")
+	var found bool
+	commandName, profileName, found = strings.Cut(filename, "@")
+	if !found {
+		return "", "", fmt.Errorf("invalid filename format: missing '@' separator")
+	}
 	profileName = strings.TrimPrefix(profileName, "profile-")
+	if profileName == "" {
+		return "", "", fmt.Errorf("invalid filename format: missing profile name")
+	}
 	return
 }
shell/command_test.go (1)

27-49: LGTM! Improved test setup with better resource management.

The changes enhance the test environment setup with proper temporary directory handling and cleanup. The use of an anonymous function for managing defer statements is a good practice.

Consider enhancing the error logging by using t.Logf instead of fmt.Printf for the temporary directory message, as it would integrate better with the test output:

-		fmt.Printf("using temporary dir: %q\n", tempDir)
+		t.Logf("using temporary dir: %q", tempDir)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b41389 and ac1ecef.

📒 Files selected for processing (7)
  • crond/parse_event_test.go (1 hunks)
  • main_test.go (1 hunks)
  • schedule/command_arguments.go (3 hunks)
  • schedule/spaced_title.go (1 hunks)
  • shell/command_test.go (1 hunks)
  • systemd/read.go (1 hunks)
  • systemd/read_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • main_test.go
  • schedule/command_arguments.go
  • systemd/read_test.go
  • crond/parse_event_test.go
🧰 Additional context used
📓 Learnings (1)
systemd/read.go (1)
Learnt from: creativeprojects
PR: creativeprojects/resticprofile#425
File: systemd/read.go:87-91
Timestamp: 2025-02-04T21:32:13.942Z
Learning: In systemd package, error handling for integer conversion from service files is unnecessary since these files are generated programmatically by the software itself, ensuring clean data.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build and test (1.23, windows-latest)
🔇 Additional comments (3)
schedule/spaced_title.go (1)

1-3: LGTM! Build constraint is properly configured.

The build constraint is correctly placed and formatted, appropriately restricting this functionality to macOS systems.

systemd/read.go (2)

1-14: LGTM! Build constraints and imports are well-organised.

The build constraints correctly limit the file to non-Darwin and non-Windows systems where systemd is available.


16-58: Add validation for timer sections.

The function correctly reads both service and timer units, but it doesn't validate whether the timer sections contain the expected configuration. Consider adding validation to ensure the timer configuration is complete and valid.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🔭 Outside diff range comments (1)
shell/command_test.go (1)

370-396: Consider improving flaky test handling.

While the comment documents the flakiness issue, there are better ways to handle it:

  1. Consider using environment variables to adjust timing expectations in CI.
  2. Add retries for flaky tests.

Here's a suggested improvement:

-// Flaky test on github linux runner but can't reproduce locally
+const maxTestDuration = 500 * time.Millisecond
+
+func getMaxTestDuration() time.Duration {
+    if d := os.Getenv("TEST_MAX_DURATION"); d != "" {
+        if parsed, err := time.ParseDuration(d); err == nil {
+            return parsed
+        }
+    }
+    return maxTestDuration
+}
+
 func TestInterruptShellCommand(t *testing.T) {
     t.Parallel()
+    maxRetries := 3
+    var lastErr error
+
     if platform.IsWindows() {
         t.Skip("Test not running on this platform")
     }
-    buffer := &bytes.Buffer{}
-
-    sigChan := make(chan os.Signal, 1)
-
-    cmd := NewSignalledCommand(mockBinary, []string{"test", "--sleep", "3000"}, sigChan)
-    cmd.Stdout = buffer
-
-    // Will ask us to stop in 100ms
-    go func() {
-        time.Sleep(100 * time.Millisecond)
-        sigChan <- syscall.SIGINT
-    }()
-    start := time.Now()
-    _, _, err := cmd.Run()
-    require.Error(t, err)
-
-    // check it ran for more than 100ms (but less than 500ms - the build agent can be very slow at times)
-    duration := time.Since(start)
-    assert.GreaterOrEqual(t, duration.Milliseconds(), int64(100))
-    assert.Less(t, duration.Milliseconds(), int64(500))
+
+    for i := 0; i < maxRetries; i++ {
+        buffer := &bytes.Buffer{}
+        sigChan := make(chan os.Signal, 1)
+        cmd := NewSignalledCommand(mockBinary, []string{"test", "--sleep", "3000"}, sigChan)
+        cmd.Stdout = buffer
+
+        go func() {
+            time.Sleep(100 * time.Millisecond)
+            sigChan <- syscall.SIGINT
+        }()
+
+        start := time.Now()
+        _, _, err := cmd.Run()
+        if err == nil {
+            t.Fatal("Expected an error but got none")
+        }
+
+        duration := time.Since(start)
+        if duration.Milliseconds() >= 100 && duration < getMaxTestDuration() {
+            return // Test passed
+        }
+        lastErr = fmt.Errorf("attempt %d: duration %v outside expected range", i+1, duration)
+        time.Sleep(time.Second) // Wait before retry
+    }
+    t.Fatalf("Test failed after %d attempts: %v", maxRetries, lastErr)
 }
🧹 Nitpick comments (6)
shell/command_test.go (1)

27-47: Well-structured improvements to TestMain!

The changes significantly improve resource management and error handling. The use of an anonymous function with proper defer statements ensures cleanup, while the platform-agnostic path handling enhances cross-platform compatibility.

Consider adding a comment explaining why -buildvcs=false flag is used in the go build command to improve maintainability.

commands_schedule_test.go (1)

40-40: Consider adding test cases for edge cases in schedule format.

The test configuration covers basic schedule formats, but consider adding test cases for:

  • Invalid schedule formats
  • Schedule formats with seconds
  • Schedule formats with specific dates

Also applies to: 45-45

commands_schedule.go (4)

18-20: Add version check for legacy flag warning.

Consider adding a version check to ensure the warning is only displayed for versions before 1.0:

+var currentVersion = "0.9.0" // Replace with actual version

 const (
-	legacyFlagWarning = "the --legacy flag is only temporary and will be removed in version 1.0."
+	legacyFlagWarning = "the --legacy flag is deprecated and will be removed in version 1.0. You are currently using version %s."
 )

42-53: Improve concurrent error handling in job collection.

The error handling in the job collection loop could be improved to handle concurrent profile processing:

+type profileError struct {
+    profile string
+    err     error
+}

-		if err != nil {
-			return err
-		}
+		if err != nil {
+            return fmt.Errorf("failed to process profile %s: %w", profileName, err)
+		}

93-100: Add success logging for job removal.

Consider adding success logging to provide better visibility of the removal process:

 			err = removeJobs(schedule.NewHandler(schedulerConfig), jobs)
 			if err != nil {
 				err = retryElevated(err, flags)
 			}
 			if err != nil {
 				// we keep trying to remove the other jobs
 				clog.Error(err)
+			} else {
+				clog.Infof("Successfully removed jobs for profile: %s", profileName)
 			}

155-156: Enhance status output formatting.

Consider improving the status output format for better readability:

-			clog.Infof("%s %q:", cases.Title(language.English).String(schedulable.Kind()), profileName)
+			clog.Infof("\n%s: %q", cases.Title(language.English).String(schedulable.Kind()), profileName)
+			clog.Infof("=" + strings.Repeat("=", len(profileName)+len(schedulable.Kind())+3))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac1ecef and 44785cb.

📒 Files selected for processing (6)
  • commands_schedule.go (6 hunks)
  • commands_schedule_test.go (2 hunks)
  • schedule/handler_crond_test.go (1 hunks)
  • schedule/handler_systemd_test.go (1 hunks)
  • schedule/spaced_title.go (1 hunks)
  • shell/command_test.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • schedule/spaced_title.go
  • schedule/handler_crond_test.go
  • schedule/handler_systemd_test.go
🔇 Additional comments (2)
shell/command_test.go (1)

552-554: Good improvements to temporary file handling!

The use of t.TempDir() ensures proper cleanup, and the explicit file permissions enhance security.

commands_schedule_test.go (1)

55-56: Clarify the scope of incomplete implementation.

The TODO comment is vague. Please specify:

  • What aspects of the test are incomplete?
  • Are there specific scenarios that need to be covered?
  • When will the implementation be completed?

commands_schedule_test.go Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (11)
schedule_jobs.go (2)

125-131: Consider enhancing error reporting with failure statistics.

When aggregating errors, it would be helpful to include the total number of failures in the error message.

Here's a suggested improvement:

-		if errs != nil {
-			return fmt.Errorf("failed to remove some jobs: %w", errs)
-		}
+		if errs != nil {
+			failureCount := len(strings.Split(errs.Error(), "\n"))
+			return fmt.Errorf("failed to remove %d jobs: %w", failureCount, errs)
+		}

214-237: Consider adding input validation for schedule configuration.

The scheduleToConfig function could benefit from validating critical fields like origin.Name and origin.Command before creating the configuration.

Here's a suggested improvement:

 func scheduleToConfig(sched *config.Schedule) *schedule.Config {
+	if sched == nil {
+		return nil
+	}
 	origin := sched.ScheduleOrigin()
+	if origin.Name == "" || origin.Command == "" {
+		return nil
+	}
 	if !sched.HasSchedules() {
 		// there's no schedule defined, so this record is for removal only
 		return schedule.NewRemoveOnlyConfig(origin.Name, origin.Command)
 	}
schedule/handler_crond.go (1)

125-158: Consider extracting error variable.

The method implementation looks good and aligns with the PR objectives. However, consider extracting the error check condition into a variable for better readability.

-	if configsErr != nil && len(entries) == 0 {
+	hasNoEntries := len(entries) == 0
+	if configsErr != nil && hasNoEntries {
crond/crontab.go (3)

34-53: Consider accepting filesystem in constructor

While the integration of afero.Fs is good, consider accepting it as a constructor parameter for better dependency injection:

-func NewCrontab(entries []Entry) (c *Crontab) {
+func NewCrontab(entries []Entry, fs afero.Fs) (c *Crontab) {
+	if fs == nil {
+		fs = afero.NewOsFs()
+	}
 	c = &Crontab{
 		entries: entries,
-		fs:      afero.NewOsFs(),
+		fs:      fs,
 	}

345-389: Document array indices in regex matches

The magic numbers in array indices (e.g., matches[7], matches[9]) make the code harder to maintain. Consider adding constants or comments to document what each index represents:

+	// Legacy pattern matches:
+	// [1] = time expression
+	// [3] = user
+	// [4] = workdir
+	// [5] = command line
+	// [6] = config file
+	// [7] = profile name
+	// [9] = command name
 	matches := legacyPattern.FindStringSubmatch(line)

334-336: Improve error message clarity

The error message could be more descriptive by including the actual line content:

-			errs = errors.Join(errs, fmt.Errorf("line %d: %w", lineNum+1, err))
+			errs = errors.Join(errs, fmt.Errorf("line %d %q: %w", lineNum+1, line, err))
commands_schedule_test.go (3)

51-57: Consider using a more realistic crontab example.

The hardcoded crontab example contains some unusual minute patterns (99,40) that would be invalid in a real crontab. While this is a test file, using realistic examples helps catch actual edge cases.

Consider using this more realistic example:

-10,40 * * * *	user	cd /workdir && /home/resticprofile --no-ansi --config config.yaml run-schedule backup@profile
-99,40 * * * *	user	cd /workdir && /home/resticprofile --no-ansi --config config.yaml run-schedule backup@profile
+10,40 * * * *	user	cd /workdir && /home/resticprofile --no-ansi --config config.yaml run-schedule backup@profile
+15,45 * * * *	user	cd /workdir && /home/resticprofile --no-ansi --config config.yaml run-schedule backup@profile

169-170: Add test coverage for error cases in statusScheduledJobs.

The test verifies the success path but doesn't cover error cases from statusScheduledJobs.

Consider adding test cases for:

  • Invalid crontab file permissions
  • Malformed crontab content
  • Empty crontab file

214-228: Enhance test assertion for schedule creation.

The test verifies that the schedule was created by checking the output string, but it doesn't verify the actual crontab entry.

Add assertions to verify the actual crontab content:

 assert.Contains(t, output.String(), "Original form: *-*-* *:20,50:00")
+// Verify the crontab entry
+crontabContent, err := os.ReadFile(crontab)
+require.NoError(t, err)
+assert.Contains(t, string(crontabContent), "20,50 * * * *")
commands_schedule.go (1)

18-20: Consider using semantic versioning for the legacy flag warning.

The warning message mentions "version 1.0" which could be ambiguous.

Use semantic versioning format:

-	legacyFlagWarning = "the --legacy flag is only temporary and will be removed in version 1.0."
+	legacyFlagWarning = "the --legacy flag is only temporary and will be removed in version 1.0.0."
commands_test.go (1)

353-363: Consider testing edge cases in TestCreateScheduleAll.

The test only verifies the happy path with a default profile.

Add test cases for:

  • Multiple profiles with schedules
  • Profiles with invalid schedules
  • Empty profile list
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44785cb and 623b5b2.

📒 Files selected for processing (7)
  • commands_schedule.go (4 hunks)
  • commands_schedule_test.go (3 hunks)
  • commands_test.go (6 hunks)
  • crond/crontab.go (8 hunks)
  • schedule/handler.go (1 hunks)
  • schedule/handler_crond.go (6 hunks)
  • schedule_jobs.go (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • schedule/handler.go
🧰 Additional context used
📓 Learnings (1)
commands_schedule_test.go (1)
Learnt from: creativeprojects
PR: creativeprojects/resticprofile#425
File: commands_schedule_test.go:49-53
Timestamp: 2025-02-05T20:08:12.633Z
Learning: In unit tests, simple hardcoded examples are preferred over parameterized ones when the goal is to test specific functionality (like schedule parsing) rather than configuration variations. Over-parameterization can unnecessarily complicate test cases.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build and test (1.23, windows-latest)
🔇 Additional comments (13)
schedule_jobs.go (2)

13-13: Well-structured handler initialisation pattern!

The consistent pattern of initialising the handler with proper cleanup using defer handler.Close() is a good practice for resource management.

Also applies to: 23-27


78-84: Good use of specific error types for better error handling!

The switch to using schedule.ErrScheduledJobNotFound makes the error handling more precise and maintainable.

schedule/handler_crond.go (7)

4-11: LGTM!

The new imports are correctly added and used throughout the code.


18-20: LGTM!

The fs field is correctly added to the struct and properly initialized in the constructor.


31-34: LGTM!

The constructor now properly initializes all fields.


54-61: LGTM!

The method signature and implementation are correctly updated to include profile information.


84-87: LGTM!

The method chain is well-structured and correctly sets up the crontab with filesystem access.


106-109: LGTM!

The changes maintain consistency with CreateJob and improve error handling clarity.

Also applies to: 115-115


162-169: LGTM!

Only formatting changes, no functional impact.

crond/crontab.go (3)

14-32: Well-structured regex patterns with performance optimisation!

The regex patterns are well-organised, broken down into reusable components, and pre-compiled for better performance.


55-72: Great API improvements with method chaining!

The method chaining support improves API usability, and the encapsulation of the generate method is a good practice.


324-343: 🛠️ Refactor suggestion

Add nil check for error handling

The parseEntries function could have a nil pointer dereference if errs is nil. Consider adding a nil check:

-	return entries, errs
+	if len(entries) == 0 && errs != nil {
+		return nil, errs
+	}
+	return entries, errs

Likely invalid or redundant comment.

commands_test.go (1)

17-17: Good addition of require package.

Using require.NoError instead of assert.NoError for prerequisite checks improves test clarity by failing fast.

commands_schedule.go Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
schedule_jobs.go (2)

96-134: Consider extracting common configuration loading logic.

Both removeScheduledJobs and statusScheduledJobs share similar configuration loading and validation logic. Consider extracting this into a helper function to reduce code duplication.

+func loadScheduledConfigs(handler schedule.Handler, configFile, profileName string) ([]schedule.Config, error) {
+    clog.Debugf("looking up schedules from configuration file %s", configFile)
+    configs, err := handler.Scheduled(profileName)
+    hasNoEntries := len(configs) == 0
+    if err != nil {
+        if hasNoEntries {
+            return nil, err
+        }
+        clog.Errorf("some configurations failed to load:\n%v", err)
+    }
+    if hasNoEntries {
+        clog.Info("no scheduled jobs found")
+        return nil, nil
+    }
+    return configs, nil
+}

Also applies to: 171-214


216-239: Consider adding documentation for configuration fields.

The scheduleToConfig function handles complex configuration mapping. Consider adding documentation to explain the purpose and requirements of each field, especially system-specific settings like SystemdDropInFiles and AfterNetworkOnline.

commands_schedule_test.go (1)

59-288: Consider enhancing test coverage and assertions.

While the test coverage is good, consider these improvements:

  1. Convert TestRemoveScheduleIntegrationUsingCrontab and TestCreateScheduleIntegrationUsingCrontab to table-driven tests for better coverage of edge cases.
  2. Add more specific assertions beyond just string contains checks.

Example of table-driven test structure:

-func TestRemoveScheduleIntegrationUsingCrontab(t *testing.T) {
+func TestRemoveScheduleIntegrationUsingCrontab(t *testing.T) {
+    testCases := []struct {
+        name        string
+        crontab     string
+        profileName string
+        wantErr     error
+        wantContent string
+    }{
+        {
+            name:        "remove existing schedule",
+            crontab:     scheduleIntegrationTestsCrontab,
+            profileName: "profile",
+            wantErr:     nil,
+            wantContent: "11,41 * * * *\tdifferent-script.sh",
+        },
+        // Add more test cases
+    }
+
+    for _, tc := range testCases {
+        t.Run(tc.name, func(t *testing.T) {
+            // Test implementation
+        })
+    }
commands_schedule.go (2)

18-20: Enhance deprecation warning message.

The warning message could be more helpful by including guidance on migration.

-const (
-    legacyFlagWarning = "the --legacy flag is only temporary and will be removed in version 1.0.0"
-)
+const (
+    legacyFlagWarning = "the --legacy flag is only temporary and will be removed in version 1.0.0. Please migrate to the new scheduling format using the standard flags"
+)

224-243: Standardise error message format.

Error messages in getProfileScheduleJobs and getGroupScheduleJobs use inconsistent formats.

-return nil, nil, fmt.Errorf("profile '%s': %w", profileName, err)
+return nil, nil, fmt.Errorf("profile %q: %w", profileName, err)

-return nil, nil, fmt.Errorf("cannot load profile '%s': %w", profileName, err)
+return nil, nil, fmt.Errorf("cannot load profile %q: %w", profileName, err)

-return nil, nil, fmt.Errorf("group '%s' not found", profileName)
+return nil, nil, fmt.Errorf("group %q: %w", profileName, config.ErrNotFound)

-return nil, nil, fmt.Errorf("cannot load group '%s': %w", profileName, err)
+return nil, nil, fmt.Errorf("cannot load group %q: %w", profileName, err)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 623b5b2 and 003d987.

📒 Files selected for processing (3)
  • commands_schedule.go (4 hunks)
  • commands_schedule_test.go (3 hunks)
  • schedule_jobs.go (8 hunks)
🧰 Additional context used
📓 Learnings (1)
commands_schedule_test.go (1)
Learnt from: creativeprojects
PR: creativeprojects/resticprofile#425
File: commands_schedule_test.go:49-53
Timestamp: 2025-02-05T20:08:12.633Z
Learning: In unit tests, simple hardcoded examples are preferred over parameterized ones when the goal is to test specific functionality (like schedule parsing) rather than configuration variations. Over-parameterization can unnecessarily complicate test cases.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build and test (1.23, windows-latest)
🔇 Additional comments (5)
schedule_jobs.go (3)

13-28: Well-structured initialization pattern!

The consistent use of handler.Init() followed by defer handler.Close() across functions ensures proper resource management.

Also applies to: 59-65


46-55: Improved job management with clear error messaging!

The code effectively handles different job states and provides informative feedback. The special handling of RemoveOnly jobs prevents unnecessary errors.

Also applies to: 68-92


78-84: Excellent error handling improvements!

The code now provides more granular error handling with specific error types and aggregates multiple errors effectively using errors.Join. This ensures users receive comprehensive feedback about all failures.

Also applies to: 148-157, 201-212

commands_schedule_test.go (2)

18-49: LGTM! Well-structured test configuration.

The configuration covers various scheduling scenarios and follows the principle of using simple, hardcoded examples for clarity in tests.


51-57: LGTM! Good test coverage with edge cases.

The crontab entries include both valid and invalid cases, which is excellent for testing error handling and edge cases.

Comment on lines 114 to 169
func statusSchedule(w io.Writer, ctx commandContext) error {
c := ctx.config
flags := ctx.flags
request := ctx.request
args := ctx.request.arguments

defer c.DisplayConfigurationIssues()

if !slices.Contains(args, "--all") {
scheduler, schedules, _, err := getScheduleJobs(c, flags)
if err != nil {
return err
}
if len(schedules) == 0 {
clog.Warningf("profile or group %s has no schedule", flags.name)
if slices.Contains(args, "--legacy") {
clog.Warning(legacyFlagWarning)
// single profile or group
if !slices.Contains(args, "--all") {
schedulerConfig, schedules, _, err := getScheduleJobs(c, request.profile)
if err != nil {
return err
}
if len(schedules) == 0 {
clog.Warningf("profile or group %s has no schedule", request.profile)
return nil
}
err = statusScheduleProfileOrGroup(schedulerConfig, schedules, ctx.flags, request.profile)
if err != nil {
return err
}
return nil
}
err = statusScheduleProfileOrGroup(scheduler, schedules, flags)
if err != nil {
return err
}
}

for _, profileName := range selectProfilesAndGroups(c, flags, args) {
profileFlags := flagsForProfile(flags, profileName)
scheduler, schedules, schedulable, err := getScheduleJobs(c, profileFlags)
if err != nil {
return err
}
// it's all fine if this profile has no schedule
if len(schedules) == 0 {
continue
}
clog.Infof("%s %q:", cases.Title(language.English).String(schedulable.Kind()), profileName)
err = statusScheduleProfileOrGroup(scheduler, schedules, profileFlags)
if err != nil {
// display the error but keep going with the other profiles
clog.Error(err)
// all profiles and groups
for _, profileName := range selectProfilesAndGroups(c, request.profile, args) {
scheduler, schedules, schedulable, err := getScheduleJobs(c, profileName)
if err != nil {
return err
}
// it's all fine if this profile has no schedule
if len(schedules) == 0 {
continue
}
clog.Infof("%s %q:", cases.Title(language.English).String(schedulable.Kind()), profileName)
err = statusScheduleProfileOrGroup(scheduler, schedules, ctx.flags, profileName)
if err != nil {
// display the error but keep going with the other profiles
clog.Error(err)
}
}
}
profileName := ctx.request.profile
if slices.Contains(args, "--all") {
// display all jobs of all profiles
profileName = ""
}
schedulerConfig := schedule.NewSchedulerConfig(ctx.global)
err := statusScheduledJobs(schedule.NewHandler(schedulerConfig), ctx.config.GetConfigFile(), profileName)
if err != nil {
return retryElevated(err, ctx.flags)
}
return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor statusSchedule for better maintainability.

The function is too long and handles too many responsibilities. Consider splitting it into smaller, focused functions.

  1. Extract legacy handling:
func handleLegacyStatusSchedule(ctx commandContext) error {
    if !slices.Contains(ctx.request.arguments, "--all") {
        return handleSingleProfileStatus(ctx)
    }
    return handleAllProfilesStatus(ctx)
}
  1. Extract new format handling:
func handleNewFormatStatusSchedule(ctx commandContext) error {
    schedulerConfig := schedule.NewSchedulerConfig(ctx.global)
    profileName := ctx.request.profile
    if slices.Contains(ctx.request.arguments, "--all") {
        profileName = ""
    }
    return statusScheduledJobs(schedule.NewHandler(schedulerConfig), ctx.config.GetConfigFile(), profileName)
}

Comment on lines 64 to 69
for _, j := range allJobs {
err := scheduleJobs(schedule.NewHandler(j.scheduler), j.name, j.jobs)
err := scheduleJobs(schedule.NewHandler(j.schedulerConfig), j.jobs)
if err != nil {
return retryElevated(err, flags)
return retryElevated(err, ctx.flags)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling in job scheduling.

The error handling could be more robust by logging errors before retrying with elevation.

 for _, j := range allJobs {
     err := scheduleJobs(schedule.NewHandler(j.schedulerConfig), j.jobs)
     if err != nil {
+        clog.Errorf("Failed to schedule jobs for %s: %v", j.name, err)
         return retryElevated(err, ctx.flags)
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, j := range allJobs {
err := scheduleJobs(schedule.NewHandler(j.scheduler), j.name, j.jobs)
err := scheduleJobs(schedule.NewHandler(j.schedulerConfig), j.jobs)
if err != nil {
return retryElevated(err, flags)
return retryElevated(err, ctx.flags)
}
}
for _, j := range allJobs {
err := scheduleJobs(schedule.NewHandler(j.schedulerConfig), j.jobs)
if err != nil {
clog.Errorf("Failed to schedule jobs for %s: %v", j.name, err)
return retryElevated(err, ctx.flags)
}
}

Comment on lines +89 to 97
err = removeJobs(schedule.NewHandler(schedulerConfig), jobs)
if err != nil {
err = retryElevated(err, ctx.flags)
}
if err != nil {
// we keep trying to remove the other jobs
clog.Error(err)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify error handling in removeJobs.

The current error handling is overly complex with nested if statements.

-err = removeJobs(schedule.NewHandler(schedulerConfig), jobs)
-if err != nil {
-    err = retryElevated(err, ctx.flags)
-}
-if err != nil {
-    // we keep trying to remove the other jobs
-    clog.Error(err)
-}
+if err := removeJobs(schedule.NewHandler(schedulerConfig), jobs); err != nil {
+    if elevatedErr := retryElevated(err, ctx.flags); elevatedErr != nil {
+        // Log error but continue with other jobs
+        clog.Errorf("Failed to remove jobs for %s: %v", profileName, elevatedErr)
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
err = removeJobs(schedule.NewHandler(schedulerConfig), jobs)
if err != nil {
err = retryElevated(err, ctx.flags)
}
if err != nil {
// we keep trying to remove the other jobs
clog.Error(err)
}
}
if err := removeJobs(schedule.NewHandler(schedulerConfig), jobs); err != nil {
if elevatedErr := retryElevated(err, ctx.flags); elevatedErr != nil {
// Log error but continue with other jobs
clog.Errorf("Failed to remove jobs for %s: %v", profileName, elevatedErr)
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Schedule "From now" negative output
1 participant