From 630d74bee7278fed9611e1d58a72646e985966c4 Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 4 Oct 2022 17:08:17 -0600 Subject: [PATCH] add NoData operation status (#1042) ## Description Adds a NoData status to operations in the event that a backup (or, possibly in the future, restore) finishes processing without having any items to store or restore. ## Type of change - [x] :sunflower: Feature ## Issue(s) * #1000 ## Test Plan - [x] :muscle: Manual - [x] :zap: Unit test --- src/internal/operations/backup.go | 4 + src/internal/operations/backup_test.go | 112 +++++++++++++-------- src/internal/operations/operation.go | 9 +- src/internal/operations/opstatus_string.go | 5 +- src/internal/operations/restore.go | 4 + src/internal/operations/restore_test.go | 108 ++++++++++++-------- 6 files changed, 154 insertions(+), 88 deletions(-) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 636fb8ca9..118a609a3 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -171,6 +171,10 @@ func (op *BackupOperation) persistResults( opStats.writeErr) } + if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 { + op.Status = NoData + } + op.Results.ReadErrors = opStats.readErr op.Results.WriteErrors = opStats.writeErr diff --git a/src/internal/operations/backup_test.go b/src/internal/operations/backup_test.go index d064aaa97..843500ab3 100644 --- a/src/internal/operations/backup_test.go +++ b/src/internal/operations/backup_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - multierror "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -35,52 +34,79 @@ func TestBackupOpSuite(t *testing.T) { } func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { - t := suite.T() - ctx := context.Background() - var ( - kw = &kopia.Wrapper{} - sw = &store.Wrapper{} - acct = account.Account{} - now = time.Now() - stats = backupStats{ - started: true, - readErr: multierror.Append(nil, assert.AnError), - writeErr: assert.AnError, - resourceCount: 1, - k: &kopia.BackupStats{ - TotalFileCount: 1, - TotalHashedBytes: 1, - TotalUploadedBytes: 1, - }, - gc: &support.ConnectorOperationStatus{ - Successful: 1, - }, - } + ctx = context.Background() + kw = &kopia.Wrapper{} + sw = &store.Wrapper{} + acct = account.Account{} + now = time.Now() ) - op, err := NewBackupOperation( - ctx, - control.Options{}, - kw, - sw, - acct, - selectors.Selector{}, - evmock.NewBus()) - require.NoError(t, err) + table := []struct { + expectStatus opStatus + expectErr assert.ErrorAssertionFunc + stats backupStats + }{ + { + expectStatus: Completed, + expectErr: assert.NoError, + stats: backupStats{ + started: true, + resourceCount: 1, + k: &kopia.BackupStats{ + TotalFileCount: 1, + TotalHashedBytes: 1, + TotalUploadedBytes: 1, + }, + gc: &support.ConnectorOperationStatus{ + Successful: 1, + }, + }, + }, + { + expectStatus: Failed, + expectErr: assert.Error, + stats: backupStats{ + started: false, + k: &kopia.BackupStats{}, + gc: &support.ConnectorOperationStatus{}, + }, + }, + { + expectStatus: NoData, + expectErr: assert.NoError, + stats: backupStats{ + started: true, + k: &kopia.BackupStats{}, + gc: &support.ConnectorOperationStatus{}, + }, + }, + } + for _, test := range table { + suite.T().Run(test.expectStatus.String(), func(t *testing.T) { + op, err := NewBackupOperation( + ctx, + control.Options{}, + kw, + sw, + acct, + selectors.Selector{}, + evmock.NewBus()) + require.NoError(t, err) + test.expectErr(t, op.persistResults(now, &test.stats)) - require.NoError(t, op.persistResults(now, &stats)) - - assert.Equal(t, op.Status.String(), Completed.String(), "status") - assert.Equal(t, op.Results.ItemsRead, stats.gc.Successful, "items read") - assert.Equal(t, op.Results.ReadErrors, stats.readErr, "read errors") - assert.Equal(t, op.Results.ItemsWritten, stats.k.TotalFileCount, "items written") - assert.Equal(t, stats.k.TotalHashedBytes, op.Results.BytesRead, "bytes read") - assert.Equal(t, stats.k.TotalUploadedBytes, op.Results.BytesUploaded, "bytes written") - assert.Equal(t, op.Results.ResourceOwners, stats.resourceCount, "resource owners") - assert.Equal(t, op.Results.WriteErrors, stats.writeErr, "write errors") - assert.Equal(t, op.Results.StartedAt, now, "started at") - assert.Less(t, now, op.Results.CompletedAt, "completed at") + assert.Equal(t, test.expectStatus.String(), op.Status.String(), "status") + assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsRead, "items read") + assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors") + assert.Equal(t, test.stats.k.TotalFileCount, op.Results.ItemsWritten, "items written") + assert.Equal(t, test.stats.k.TotalHashedBytes, op.Results.BytesRead, "bytes read") + assert.Equal(t, test.stats.k.TotalUploadedBytes, op.Results.BytesUploaded, "bytes written") + assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners") + assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors") + assert.Equal(t, now, op.Results.StartedAt, "started at") + assert.Less(t, now, op.Results.CompletedAt, "completed at") + }) + } } // --------------------------------------------------------------------------- diff --git a/src/internal/operations/operation.go b/src/internal/operations/operation.go index 1ced4395a..efe6f3c41 100644 --- a/src/internal/operations/operation.go +++ b/src/internal/operations/operation.go @@ -13,13 +13,17 @@ import ( // opStatus describes the current status of an operation. // InProgress - the standard value for any process that has not -// arrived at an end state. The two end states are Failed and -// Completed. +// arrived at an end state. The end states are Failed, Completed, +// or NoData. // Failed - the operation was unable to begin processing data at all. // No items have been written by the consumer. // Completed - the operation was able to process one or more of the // items in the request. Both partial success (0 < N < len(items) // errored) and total success (0 errors) are set as Completed. +// NoData - only occurs when no data was involved in an operation. +// For example, if a backup is requested for a specific user's +// mail, but that account contains zero mail messages, the backup +// contains No Data. type opStatus int //go:generate stringer -type=opStatus -linecomment @@ -28,6 +32,7 @@ const ( InProgress // In Progress Completed // Completed Failed // Failed + NoData // No Data ) // -------------------------------------------------------------------------------- diff --git a/src/internal/operations/opstatus_string.go b/src/internal/operations/opstatus_string.go index 9277e6203..7cd244131 100644 --- a/src/internal/operations/opstatus_string.go +++ b/src/internal/operations/opstatus_string.go @@ -12,11 +12,12 @@ func _() { _ = x[InProgress-1] _ = x[Completed-2] _ = x[Failed-3] + _ = x[NoData-4] } -const _opStatus_name = "Status UnknownIn ProgressCompletedFailed" +const _opStatus_name = "Status UnknownIn ProgressCompletedFailedNo Data" -var _opStatus_index = [...]uint8{0, 14, 25, 34, 40} +var _opStatus_index = [...]uint8{0, 14, 25, 34, 40, 47} func (i opStatus) String() string { if i < 0 || i >= opStatus(len(_opStatus_index)-1) { diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 6cd5a7799..5180e59aa 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -195,6 +195,10 @@ func (op *RestoreOperation) persistResults( opStats.writeErr) } + if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 { + op.Status = NoData + } + op.Results.ReadErrors = opStats.readErr op.Results.WriteErrors = opStats.writeErr diff --git a/src/internal/operations/restore_test.go b/src/internal/operations/restore_test.go index caa3c6aab..221a77e15 100644 --- a/src/internal/operations/restore_test.go +++ b/src/internal/operations/restore_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - multierror "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -38,56 +37,83 @@ func TestRestoreOpSuite(t *testing.T) { suite.Run(t, new(RestoreOpSuite)) } -// TODO: after modelStore integration is added, mock the store and/or -// move this to an integration test. func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { var ( - t = suite.T() - ctx = context.Background() - + ctx = context.Background() kw = &kopia.Wrapper{} sw = &store.Wrapper{} acct = account.Account{} now = time.Now() - rs = restoreStats{ - started: true, - readErr: multierror.Append(nil, assert.AnError), - writeErr: assert.AnError, - resourceCount: 1, - bytesRead: &stats.ByteCounter{ - NumBytes: 42, - }, - cs: []data.Collection{&exchange.Collection{}}, - gc: &support.ConnectorOperationStatus{ - ObjectCount: 1, - }, - } dest = control.DefaultRestoreDestination(common.SimpleDateTimeFormat) ) - op, err := NewRestoreOperation( - ctx, - control.Options{}, - kw, - sw, - acct, - "foo", - selectors.Selector{}, - dest, - evmock.NewBus()) - require.NoError(t, err) + table := []struct { + expectStatus opStatus + expectErr assert.ErrorAssertionFunc + stats restoreStats + }{ + { + expectStatus: Completed, + expectErr: assert.NoError, + stats: restoreStats{ + started: true, + resourceCount: 1, + bytesRead: &stats.ByteCounter{ + NumBytes: 42, + }, + cs: []data.Collection{&exchange.Collection{}}, + gc: &support.ConnectorOperationStatus{ + ObjectCount: 1, + Successful: 1, + }, + }, + }, + { + expectStatus: Failed, + expectErr: assert.Error, + stats: restoreStats{ + started: false, + bytesRead: &stats.ByteCounter{}, + gc: &support.ConnectorOperationStatus{}, + }, + }, + { + expectStatus: NoData, + expectErr: assert.NoError, + stats: restoreStats{ + started: true, + bytesRead: &stats.ByteCounter{}, + cs: []data.Collection{}, + gc: &support.ConnectorOperationStatus{}, + }, + }, + } + for _, test := range table { + suite.T().Run(test.expectStatus.String(), func(t *testing.T) { + op, err := NewRestoreOperation( + ctx, + control.Options{}, + kw, + sw, + acct, + "foo", + selectors.Selector{}, + dest, + evmock.NewBus()) + require.NoError(t, err) + test.expectErr(t, op.persistResults(ctx, now, &test.stats)) - require.NoError(t, op.persistResults(ctx, now, &rs)) - - assert.Equal(t, op.Status.String(), Completed.String(), "status") - assert.Equal(t, op.Results.ItemsRead, len(rs.cs), "items read") - assert.Equal(t, op.Results.ReadErrors, rs.readErr, "read errors") - assert.Equal(t, op.Results.ItemsWritten, rs.gc.Successful, "items written") - assert.Equal(t, rs.bytesRead.NumBytes, op.Results.BytesRead, "resource owners") - assert.Equal(t, rs.resourceCount, op.Results.ResourceOwners, "resource owners") - assert.Equal(t, op.Results.WriteErrors, rs.writeErr, "write errors") - assert.Equal(t, op.Results.StartedAt, now, "started at") - assert.Less(t, now, op.Results.CompletedAt, "completed at") + assert.Equal(t, test.expectStatus.String(), op.Status.String(), "status") + assert.Equal(t, len(test.stats.cs), op.Results.ItemsRead, "items read") + assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors") + assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsWritten, "items written") + assert.Equal(t, test.stats.bytesRead.NumBytes, op.Results.BytesRead, "resource owners") + assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners") + assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors") + assert.Equal(t, now, op.Results.StartedAt, "started at") + assert.Less(t, now, op.Results.CompletedAt, "completed at") + }) + } } // ---------------------------------------------------------------------------