diff --git a/.golangci.yml b/.golangci.yml index bbfb0c5..60709d4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -44,6 +44,9 @@ linters: varnamelen: ignore-decls: - fs afero.Fs + wrapcheck: + ignore-package-globs: + - backup-rsync/* formatters: enable: - gofmt diff --git a/backup/cmd/check-coverage.go b/backup/cmd/check-coverage.go index 2af8113..c0d775d 100644 --- a/backup/cmd/check-coverage.go +++ b/backup/cmd/check-coverage.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "log" "os" "backup-rsync/backup/internal" @@ -24,7 +23,7 @@ func buildCheckCoverageCommand(fs afero.Fs) *cobra.Command { } checker := &internal.CoverageChecker{ - Logger: log.New(os.Stderr, "", log.LstdFlags), + Logger: internal.NewUTCLogger(os.Stderr), Fs: fs, } diff --git a/backup/cmd/job.go b/backup/cmd/job.go index 710da55..b3e8b88 100644 --- a/backup/cmd/job.go +++ b/backup/cmd/job.go @@ -11,12 +11,18 @@ import ( "github.com/spf13/cobra" ) +// LoggerFactory creates a logger, returning the logger, log directory path, cleanup function, and any error. +type LoggerFactory func(fs afero.Fs, configPath string, now time.Time) (*log.Logger, string, func() error, error) + +func discardLoggerFactory(_ afero.Fs, _ string, _ time.Time) (*log.Logger, string, func() error, error) { + return log.New(io.Discard, "", 0), "", func() error { return nil }, nil +} + type jobCommandOptions struct { - use string - short string - needsLog bool - simulate bool - factory func(rsyncPath string, logPath string, out io.Writer) internal.JobCommand + use string + short string + factory func(rsyncPath string, logPath string, out io.Writer) internal.JobCommand + createLogger LoggerFactory } func buildJobCommand(fs afero.Fs, opts jobCommandOptions) *cobra.Command { @@ -33,24 +39,22 @@ func buildJobCommand(fs afero.Fs, opts jobCommandOptions) *cobra.Command { } out := cmd.OutOrStdout() - logger := log.New(io.Discard, "", 0) - - var logPath string - if opts.needsLog { - var cleanup func() error - - logger, logPath, cleanup, err = internal.CreateMainLogger(fs, configPath, opts.simulate, time.Now()) - if err != nil { - return fmt.Errorf("creating logger: %w", err) - } + createLogger := opts.createLogger + if createLogger == nil { + createLogger = discardLoggerFactory + } - defer cleanup() + logger, logPath, cleanup, err := createLogger(fs, configPath, time.Now()) + if err != nil { + return fmt.Errorf("creating logger: %w", err) } + defer cleanup() + command := opts.factory(rsyncPath, logPath, out) - return cfg.Apply(command, logger, out) + return cfg.Apply(command, logger) }, } } diff --git a/backup/cmd/run.go b/backup/cmd/run.go index d9f0923..fae60fe 100644 --- a/backup/cmd/run.go +++ b/backup/cmd/run.go @@ -3,6 +3,8 @@ package cmd import ( "backup-rsync/backup/internal" "io" + "log" + "time" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -10,9 +12,15 @@ import ( func buildRunCommand(fs afero.Fs, shell internal.Exec) *cobra.Command { return buildJobCommand(fs, jobCommandOptions{ - use: "run", - short: "Execute the sync jobs", - needsLog: true, + use: "run", + short: "Execute the sync jobs", + createLogger: func(fs afero.Fs, configPath string, now time.Time) (*log.Logger, string, func() error, error) { + logPath := internal.GetLogPath(configPath, now) + + logger, cleanup, err := internal.CreateMainLogger(fs, logPath) + + return logger, logPath, cleanup, err + }, factory: func(rsyncPath string, logPath string, out io.Writer) internal.JobCommand { return internal.NewSyncCommand(rsyncPath, logPath, shell, out) }, diff --git a/backup/cmd/simulate.go b/backup/cmd/simulate.go index 441d7c8..dac1a54 100644 --- a/backup/cmd/simulate.go +++ b/backup/cmd/simulate.go @@ -3,6 +3,8 @@ package cmd import ( "backup-rsync/backup/internal" "io" + "log" + "time" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -10,10 +12,15 @@ import ( func buildSimulateCommand(fs afero.Fs, shell internal.Exec) *cobra.Command { return buildJobCommand(fs, jobCommandOptions{ - use: "simulate", - short: "Simulate the sync jobs", - needsLog: true, - simulate: true, + use: "simulate", + short: "Simulate the sync jobs", + createLogger: func(fs afero.Fs, configPath string, now time.Time) (*log.Logger, string, func() error, error) { + logPath := internal.GetLogPath(configPath, now) + "-sim" + + logger, cleanup, err := internal.CreateMainLogger(fs, logPath) + + return logger, logPath, cleanup, err + }, factory: func(rsyncPath string, logPath string, out io.Writer) internal.JobCommand { return internal.NewSimulateCommand(rsyncPath, logPath, shell, out) }, diff --git a/backup/cmd/test/commands_test.go b/backup/cmd/test/commands_test.go index df2262d..aab5365 100644 --- a/backup/cmd/test/commands_test.go +++ b/backup/cmd/test/commands_test.go @@ -310,7 +310,8 @@ func TestList_ValidConfig(t *testing.T) { require.NoError(t, err) assert.Contains(t, stdout, "Job: docs") - assert.Contains(t, stdout, "Status [docs]: SUCCESS") + assert.NotContains(t, stdout, "Status [docs]:") + assert.NotContains(t, stdout, "Summary:") } // --- run: logger cleanup happens after cfg.Apply completes --- diff --git a/backup/cmd/test/integration_test.go b/backup/cmd/test/integration_test.go index 7a21a4a..171a95e 100644 --- a/backup/cmd/test/integration_test.go +++ b/backup/cmd/test/integration_test.go @@ -362,7 +362,8 @@ func TestIntegration_List_ShowsCommands(t *testing.T) { assert.Contains(t, stdout, "--exclude=temp") assert.Contains(t, stdout, src+"/") assert.Contains(t, stdout, dst+"/") - assert.Contains(t, stdout, "Status [listjob]: SUCCESS") + assert.NotContains(t, stdout, "Status [listjob]:") + assert.NotContains(t, stdout, "Summary:") // list should not actually sync files assert.False(t, fileExists(t, filepath.Join(dst, "x.txt")), "list should not sync files") diff --git a/backup/internal/config.go b/backup/internal/config.go index d0c920b..1d853f0 100644 --- a/backup/internal/config.go +++ b/backup/internal/config.go @@ -39,7 +39,7 @@ func (cfg Config) String() string { return string(out) } -func (cfg Config) Apply(rsync JobCommand, logger *log.Logger, output io.Writer) error { +func (cfg Config) Apply(rsync JobCommand, logger *log.Logger) error { versionInfo, fullpath, err := rsync.GetVersionInfo() if err != nil { logger.Printf("Failed to fetch rsync version: %v", err) @@ -52,15 +52,11 @@ func (cfg Config) Apply(rsync JobCommand, logger *log.Logger, output io.Writer) for _, job := range cfg.Jobs { status := job.Apply(rsync) - logger.Printf("STATUS [%s]: %s", job.Name, status) - fmt.Fprintf(output, "Status [%s]: %s\n", job.Name, status) + rsync.ReportJobStatus(job.Name, status, logger) counts[status]++ } - summary := fmt.Sprintf("Summary: %d succeeded, %d failed, %d skipped", - counts[Success], counts[Failure], counts[Skipped]) - logger.Print(summary) - fmt.Fprintln(output, summary) + rsync.ReportSummary(counts, logger) if counts[Failure] > 0 { return fmt.Errorf("%w: %d of %d jobs", ErrJobFailure, counts[Failure], len(cfg.Jobs)) diff --git a/backup/internal/helper.go b/backup/internal/helper.go index d89255b..b77df1a 100644 --- a/backup/internal/helper.go +++ b/backup/internal/helper.go @@ -3,6 +3,7 @@ package internal import ( "fmt" + "io" "log" "os" "path/filepath" @@ -12,6 +13,28 @@ import ( "github.com/spf13/afero" ) +// UTCLogWriter wraps an io.Writer and prepends an ISO 8601 UTC timestamp to each write. +type UTCLogWriter struct { + W io.Writer + Now func() time.Time +} + +func (u *UTCLogWriter) Write(data []byte) (int, error) { + now := u.Now().UTC().Format(time.RFC3339) + + _, err := fmt.Fprintf(u.W, "%s %s", now, data) + if err != nil { + return 0, fmt.Errorf("writing log entry: %w", err) + } + + return len(data), nil +} + +// NewUTCLogger creates a *log.Logger that writes ISO 8601 UTC timestamps. +func NewUTCLogger(w io.Writer) *log.Logger { + return log.New(&UTCLogWriter{W: w, Now: time.Now}, "", 0) +} + // Path represents a source or target path with optional exclusions. type Path struct { Path string `yaml:"path"` @@ -25,39 +48,33 @@ func NormalizePath(path string) string { const LogFilePermission = 0644 const LogDirPermission = 0755 -func getLogPath(simulate bool, configPath string, now time.Time) string { +func GetLogPath(configPath string, now time.Time) string { filename := filepath.Base(configPath) filename = strings.TrimSuffix(filename, ".yaml") - logPath := "logs/sync-" + now.Format("2006-01-02T15-04-05") + "-" + filename - - if simulate { - logPath += "-sim" - } - return logPath + return "logs/sync-" + now.Format("2006-01-02T15-04-05") + "-" + filename } func CreateMainLogger( - fs afero.Fs, configPath string, simulate bool, now time.Time, -) (*log.Logger, string, func() error, error) { - logPath := getLogPath(simulate, configPath, now) + fs afero.Fs, logPath string, +) (*log.Logger, func() error, error) { overallLogPath := logPath + "/summary.log" err := fs.MkdirAll(logPath, LogDirPermission) if err != nil { - return nil, "", nil, fmt.Errorf("failed to create log directory: %w", err) + return nil, nil, fmt.Errorf("failed to create log directory: %w", err) } overallLogFile, err := fs.OpenFile(overallLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, LogFilePermission) if err != nil { - return nil, "", nil, fmt.Errorf("failed to open overall log file: %w", err) + return nil, nil, fmt.Errorf("failed to open overall log file: %w", err) } - logger := log.New(overallLogFile, "", log.LstdFlags) + logger := NewUTCLogger(overallLogFile) cleanup := func() error { return overallLogFile.Close() } - return logger, logPath, cleanup, nil + return logger, cleanup, nil } diff --git a/backup/internal/job_command.go b/backup/internal/job_command.go index 80e7be7..8441463 100644 --- a/backup/internal/job_command.go +++ b/backup/internal/job_command.go @@ -1,5 +1,7 @@ package internal +import "log" + // JobStatus represents the outcome of a job execution. type JobStatus string @@ -16,4 +18,6 @@ const ( type JobCommand interface { Run(job Job) JobStatus GetVersionInfo() (string, string, error) + ReportJobStatus(jobName string, status JobStatus, logger *log.Logger) + ReportSummary(counts map[JobStatus]int, logger *log.Logger) } diff --git a/backup/internal/rsync.go b/backup/internal/rsync.go index 73d3b43..3e439ae 100644 --- a/backup/internal/rsync.go +++ b/backup/internal/rsync.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "log" "os" "path/filepath" "strings" @@ -42,6 +43,18 @@ func (c SharedCommand) PrintArgs(job Job, args []string) { fmt.Fprintf(c.Output, "Command: %s %s\n", c.BinPath, strings.Join(args, " ")) } +func (c SharedCommand) ReportJobStatus(jobName string, status JobStatus, logger *log.Logger) { + logger.Printf("STATUS [%s]: %s", jobName, status) + fmt.Fprintf(c.Output, "Status [%s]: %s\n", jobName, status) +} + +func (c SharedCommand) ReportSummary(counts map[JobStatus]int, logger *log.Logger) { + summary := fmt.Sprintf("Summary: %d succeeded, %d failed, %d skipped", + counts[Success], counts[Failure], counts[Skipped]) + logger.Print(summary) + fmt.Fprintln(c.Output, summary) +} + func (c SharedCommand) RunWithArgs(job Job, args []string) JobStatus { c.PrintArgs(job, args) diff --git a/backup/internal/rsync_list.go b/backup/internal/rsync_list.go index 23b39fe..bf3f97b 100644 --- a/backup/internal/rsync_list.go +++ b/backup/internal/rsync_list.go @@ -1,6 +1,9 @@ package internal -import "io" +import ( + "io" + "log" +) // ListCommand prints the rsync commands that would be executed without running them. type ListCommand struct { @@ -14,6 +17,10 @@ func NewListCommand(binPath string, shell Exec, output io.Writer) ListCommand { } } +func (ListCommand) ReportJobStatus(_ string, _ JobStatus, _ *log.Logger) {} + +func (ListCommand) ReportSummary(_ map[JobStatus]int, _ *log.Logger) {} + func (c ListCommand) Run(job Job) JobStatus { logPath := c.JobLogPath(job) args := ArgumentsForJob(job, logPath, false) diff --git a/backup/internal/test/config_test.go b/backup/internal/test/config_test.go index 670c58e..dbfe170 100644 --- a/backup/internal/test/config_test.go +++ b/backup/internal/test/config_test.go @@ -367,8 +367,6 @@ func TestLoadResolvedConfig(t *testing.T) { func TestConfigApply_VersionInfoSuccess(t *testing.T) { mockCmd := NewMockJobCommand(t) - var output bytes.Buffer - var logBuf bytes.Buffer logger := log.New(&logBuf, "", 0) @@ -382,24 +380,20 @@ func TestConfigApply_VersionInfoSuccess(t *testing.T) { mockCmd.EXPECT().GetVersionInfo().Return("rsync version 3.2.3", "/usr/bin/rsync", nil).Once() mockCmd.EXPECT().Run(mock.AnythingOfType("internal.Job")).Return(Success).Once() + mockCmd.EXPECT().ReportJobStatus("job1", Success, logger).Once() + mockCmd.EXPECT().ReportJobStatus("job2", Skipped, logger).Once() + mockCmd.EXPECT().ReportSummary(map[JobStatus]int{Success: 1, Skipped: 1}, logger).Once() - err := cfg.Apply(mockCmd, logger, &output) + err := cfg.Apply(mockCmd, logger) require.NoError(t, err) assert.Contains(t, logBuf.String(), "Rsync Binary Path: /usr/bin/rsync") assert.Contains(t, logBuf.String(), "Rsync Version Info: rsync version 3.2.3") - assert.Contains(t, logBuf.String(), "STATUS [job1]: SUCCESS") - assert.Contains(t, logBuf.String(), "STATUS [job2]: SKIPPED") - assert.Contains(t, output.String(), "Status [job1]: SUCCESS") - assert.Contains(t, output.String(), "Status [job2]: SKIPPED") - assert.Contains(t, output.String(), "Summary: 1 succeeded, 0 failed, 1 skipped") } func TestConfigApply_VersionInfoError(t *testing.T) { mockCmd := NewMockJobCommand(t) - var output bytes.Buffer - var logBuf bytes.Buffer logger := log.New(&logBuf, "", 0) @@ -412,14 +406,13 @@ func TestConfigApply_VersionInfoError(t *testing.T) { mockCmd.EXPECT().GetVersionInfo().Return("", "", errCommandNotFound).Once() mockCmd.EXPECT().Run(mock.AnythingOfType("internal.Job")).Return(Failure).Once() + mockCmd.EXPECT().ReportJobStatus("backup", Failure, logger).Once() + mockCmd.EXPECT().ReportSummary(map[JobStatus]int{Failure: 1}, logger).Once() - err := cfg.Apply(mockCmd, logger, &output) + err := cfg.Apply(mockCmd, logger) require.Error(t, err) require.ErrorIs(t, err, ErrJobFailure) assert.Contains(t, logBuf.String(), "Failed to fetch rsync version: command not found") assert.NotContains(t, logBuf.String(), "Rsync Binary Path") - assert.Contains(t, logBuf.String(), "STATUS [backup]: FAILURE") - assert.Contains(t, output.String(), "Status [backup]: FAILURE") - assert.Contains(t, output.String(), "Summary: 0 succeeded, 1 failed, 0 skipped") } diff --git a/backup/internal/test/helper_test.go b/backup/internal/test/helper_test.go index b28489a..7d1fd40 100644 --- a/backup/internal/test/helper_test.go +++ b/backup/internal/test/helper_test.go @@ -1,6 +1,8 @@ package internal_test import ( + "bytes" + "errors" "testing" "time" @@ -33,7 +35,9 @@ func fixedTime() time.Time { } func TestCreateMainLogger_Title_IsPresent(t *testing.T) { - logger, logPath, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), "title", true, fixedTime()) + logPath := GetLogPath("title", fixedTime()) + + logger, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), logPath) require.NoError(t, err) defer cleanup() @@ -42,37 +46,21 @@ func TestCreateMainLogger_Title_IsPresent(t *testing.T) { assert.NotNil(t, logger) } -func TestCreateMainLogger_IsSimulate_HasSimSuffix(t *testing.T) { - logger, logPath, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), "", true, fixedTime()) - require.NoError(t, err) - - defer cleanup() - - assert.Contains(t, logPath, "-sim") - assert.NotNil(t, logger) -} +func TestCreateMainLogger_DeterministicLogPath(t *testing.T) { + logPath := GetLogPath("backup.yaml", fixedTime()) -func TestCreateMainLogger_NotSimulate_HasNoSimSuffix(t *testing.T) { - logger, logPath, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), "", false, fixedTime()) + _, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), logPath) require.NoError(t, err) defer cleanup() - assert.NotContains(t, logPath, "-sim") - assert.NotNil(t, logger) + assert.Equal(t, "logs/sync-2025-06-15T14-30-45-backup", logPath) } -func TestCreateMainLogger_DeterministicLogPath(t *testing.T) { - _, logPath, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), "backup.yaml", true, fixedTime()) - require.NoError(t, err) - - defer cleanup() - - assert.Equal(t, "logs/sync-2025-06-15T14-30-45-backup-sim", logPath) -} +func TestCreateMainLogger_DeterministicLogPath_AnotherConfig(t *testing.T) { + logPath := GetLogPath("sync.yaml", fixedTime()) -func TestCreateMainLogger_DeterministicLogPath_NoSimulate(t *testing.T) { - _, logPath, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), "sync.yaml", false, fixedTime()) + _, cleanup, err := CreateMainLogger(afero.NewMemMapFs(), logPath) require.NoError(t, err) defer cleanup() @@ -83,8 +71,9 @@ func TestCreateMainLogger_DeterministicLogPath_NoSimulate(t *testing.T) { func TestCreateMainLogger_MkdirError(t *testing.T) { // Use a read-only filesystem to block directory creation fs := afero.NewReadOnlyFs(afero.NewMemMapFs()) + logPath := GetLogPath("test.yaml", fixedTime()) - _, _, cleanup, err := CreateMainLogger(fs, "test.yaml", false, fixedTime()) + _, cleanup, err := CreateMainLogger(fs, logPath) _ = cleanup require.Error(t, err) @@ -93,10 +82,95 @@ func TestCreateMainLogger_MkdirError(t *testing.T) { func TestCreateMainLogger_OpenFileError(t *testing.T) { fs := afero.NewReadOnlyFs(afero.NewMemMapFs()) + logPath := GetLogPath("test.yaml", fixedTime()) - _, _, cleanup, err := CreateMainLogger(fs, "test.yaml", false, fixedTime()) + _, cleanup, err := CreateMainLogger(fs, logPath) _ = cleanup require.Error(t, err) assert.Contains(t, err.Error(), "failed to create log directory") } + +func TestGetLogPath(t *testing.T) { + tests := []struct { + name string + configPath string + expected string + }{ + {"WithYamlExtension", "backup.yaml", "logs/sync-2025-06-15T14-30-45-backup"}, + {"WithoutYamlExtension", "sync", "logs/sync-2025-06-15T14-30-45-sync"}, + {"WithDirectoryPrefix", "/etc/configs/media.yaml", "logs/sync-2025-06-15T14-30-45-media"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := GetLogPath(test.configPath, fixedTime()) + assert.Equal(t, test.expected, result) + }) + } +} + +func TestUTCLogWriter_FormatsISO8601UTC(t *testing.T) { + var buf bytes.Buffer + + writer := &UTCLogWriter{ + W: &buf, + Now: fixedTime, + } + + _, err := writer.Write([]byte("hello\n")) + require.NoError(t, err) + + assert.Equal(t, "2025-06-15T14:30:45Z hello\n", buf.String()) +} + +func TestUTCLogWriter_ConvertsToUTC(t *testing.T) { + var buf bytes.Buffer + + eastern := time.FixedZone("EST", -5*60*60) + nonUTCTime := time.Date(2025, 6, 15, 10, 0, 0, 0, eastern) + + writer := &UTCLogWriter{ + W: &buf, + Now: func() time.Time { + return nonUTCTime + }, + } + + _, err := writer.Write([]byte("test\n")) + require.NoError(t, err) + + assert.Equal(t, "2025-06-15T15:00:00Z test\n", buf.String()) +} + +var errWriteFailed = errors.New("write failed") + +type failWriter struct{} + +func (f *failWriter) Write(_ []byte) (int, error) { + return 0, errWriteFailed +} + +func TestUTCLogWriter_PropagatesWriteError(t *testing.T) { + writer := &UTCLogWriter{ + W: &failWriter{}, + Now: fixedTime, + } + + _, err := writer.Write([]byte("hello\n")) + + require.ErrorIs(t, err, errWriteFailed) +} + +func TestNewUTCLogger_WritesISO8601(t *testing.T) { + var buf bytes.Buffer + + logger := NewUTCLogger(&buf) + + logger.Print("test message") + + output := buf.String() + // Should contain ISO 8601 timestamp format (RFC3339) and the message + assert.Contains(t, output, "test message") + assert.Regexp(t, `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z `, output) +} diff --git a/backup/internal/test/mock_jobcommand_test.go b/backup/internal/test/mock_jobcommand_test.go index 5e911ab..68f6a16 100644 --- a/backup/internal/test/mock_jobcommand_test.go +++ b/backup/internal/test/mock_jobcommand_test.go @@ -6,6 +6,7 @@ package internal_test import ( "backup-rsync/backup/internal" + "log" mock "github.com/stretchr/testify/mock" ) @@ -96,6 +97,104 @@ func (_c *MockJobCommand_GetVersionInfo_Call) RunAndReturn(run func() (string, s return _c } +// ReportJobStatus provides a mock function for the type MockJobCommand +func (_mock *MockJobCommand) ReportJobStatus(jobName string, status internal.JobStatus, logger *log.Logger) { + _mock.Called(jobName, status, logger) + return +} + +// MockJobCommand_ReportJobStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReportJobStatus' +type MockJobCommand_ReportJobStatus_Call struct { + *mock.Call +} + +// ReportJobStatus is a helper method to define mock.On call +// - jobName string +// - status internal.JobStatus +// - logger *log.Logger +func (_e *MockJobCommand_Expecter) ReportJobStatus(jobName interface{}, status interface{}, logger interface{}) *MockJobCommand_ReportJobStatus_Call { + return &MockJobCommand_ReportJobStatus_Call{Call: _e.mock.On("ReportJobStatus", jobName, status, logger)} +} + +func (_c *MockJobCommand_ReportJobStatus_Call) Run(run func(jobName string, status internal.JobStatus, logger *log.Logger)) *MockJobCommand_ReportJobStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 internal.JobStatus + if args[1] != nil { + arg1 = args[1].(internal.JobStatus) + } + var arg2 *log.Logger + if args[2] != nil { + arg2 = args[2].(*log.Logger) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockJobCommand_ReportJobStatus_Call) Return() *MockJobCommand_ReportJobStatus_Call { + _c.Call.Return() + return _c +} + +func (_c *MockJobCommand_ReportJobStatus_Call) RunAndReturn(run func(jobName string, status internal.JobStatus, logger *log.Logger)) *MockJobCommand_ReportJobStatus_Call { + _c.Run(run) + return _c +} + +// ReportSummary provides a mock function for the type MockJobCommand +func (_mock *MockJobCommand) ReportSummary(counts map[internal.JobStatus]int, logger *log.Logger) { + _mock.Called(counts, logger) + return +} + +// MockJobCommand_ReportSummary_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReportSummary' +type MockJobCommand_ReportSummary_Call struct { + *mock.Call +} + +// ReportSummary is a helper method to define mock.On call +// - counts map[internal.JobStatus]int +// - logger *log.Logger +func (_e *MockJobCommand_Expecter) ReportSummary(counts interface{}, logger interface{}) *MockJobCommand_ReportSummary_Call { + return &MockJobCommand_ReportSummary_Call{Call: _e.mock.On("ReportSummary", counts, logger)} +} + +func (_c *MockJobCommand_ReportSummary_Call) Run(run func(counts map[internal.JobStatus]int, logger *log.Logger)) *MockJobCommand_ReportSummary_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 map[internal.JobStatus]int + if args[0] != nil { + arg0 = args[0].(map[internal.JobStatus]int) + } + var arg1 *log.Logger + if args[1] != nil { + arg1 = args[1].(*log.Logger) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockJobCommand_ReportSummary_Call) Return() *MockJobCommand_ReportSummary_Call { + _c.Call.Return() + return _c +} + +func (_c *MockJobCommand_ReportSummary_Call) RunAndReturn(run func(counts map[internal.JobStatus]int, logger *log.Logger)) *MockJobCommand_ReportSummary_Call { + _c.Run(run) + return _c +} + // Run provides a mock function for the type MockJobCommand func (_mock *MockJobCommand) Run(job internal.Job) internal.JobStatus { ret := _mock.Called(job)