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] 🌻 Feature

## Issue(s)

* #1000

## Test Plan

- [x] 💪 Manual
- [x]  Unit test
This commit is contained in:
Keepers 2022-10-04 17:08:17 -06:00 committed by GitHub
parent 3a62f7f90c
commit 630d74bee7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 154 additions and 88 deletions

View File

@ -171,6 +171,10 @@ func (op *BackupOperation) persistResults(
opStats.writeErr) opStats.writeErr)
} }
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
op.Status = NoData
}
op.Results.ReadErrors = opStats.readErr op.Results.ReadErrors = opStats.readErr
op.Results.WriteErrors = opStats.writeErr op.Results.WriteErrors = opStats.writeErr

View File

@ -5,7 +5,6 @@ import (
"testing" "testing"
"time" "time"
multierror "github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -35,52 +34,79 @@ func TestBackupOpSuite(t *testing.T) {
} }
func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { func (suite *BackupOpSuite) TestBackupOperation_PersistResults() {
t := suite.T()
ctx := context.Background()
var ( var (
kw = &kopia.Wrapper{} ctx = context.Background()
sw = &store.Wrapper{} kw = &kopia.Wrapper{}
acct = account.Account{} sw = &store.Wrapper{}
now = time.Now() acct = account.Account{}
stats = backupStats{ now = time.Now()
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,
},
}
) )
op, err := NewBackupOperation( table := []struct {
ctx, expectStatus opStatus
control.Options{}, expectErr assert.ErrorAssertionFunc
kw, stats backupStats
sw, }{
acct, {
selectors.Selector{}, expectStatus: Completed,
evmock.NewBus()) expectErr: assert.NoError,
require.NoError(t, err) 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, test.expectStatus.String(), op.Status.String(), "status")
assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsRead, "items read")
assert.Equal(t, op.Status.String(), Completed.String(), "status") assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors")
assert.Equal(t, op.Results.ItemsRead, stats.gc.Successful, "items read") assert.Equal(t, test.stats.k.TotalFileCount, op.Results.ItemsWritten, "items written")
assert.Equal(t, op.Results.ReadErrors, stats.readErr, "read errors") assert.Equal(t, test.stats.k.TotalHashedBytes, op.Results.BytesRead, "bytes read")
assert.Equal(t, op.Results.ItemsWritten, stats.k.TotalFileCount, "items written") assert.Equal(t, test.stats.k.TotalUploadedBytes, op.Results.BytesUploaded, "bytes written")
assert.Equal(t, stats.k.TotalHashedBytes, op.Results.BytesRead, "bytes read") assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners")
assert.Equal(t, stats.k.TotalUploadedBytes, op.Results.BytesUploaded, "bytes written") assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors")
assert.Equal(t, op.Results.ResourceOwners, stats.resourceCount, "resource owners") assert.Equal(t, now, op.Results.StartedAt, "started at")
assert.Equal(t, op.Results.WriteErrors, stats.writeErr, "write errors") assert.Less(t, now, op.Results.CompletedAt, "completed at")
assert.Equal(t, op.Results.StartedAt, now, "started at") })
assert.Less(t, now, op.Results.CompletedAt, "completed at") }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -13,13 +13,17 @@ import (
// opStatus describes the current status of an operation. // opStatus describes the current status of an operation.
// InProgress - the standard value for any process that has not // InProgress - the standard value for any process that has not
// arrived at an end state. The two end states are Failed and // arrived at an end state. The end states are Failed, Completed,
// Completed. // or NoData.
// Failed - the operation was unable to begin processing data at all. // Failed - the operation was unable to begin processing data at all.
// No items have been written by the consumer. // No items have been written by the consumer.
// Completed - the operation was able to process one or more of the // Completed - the operation was able to process one or more of the
// items in the request. Both partial success (0 < N < len(items) // items in the request. Both partial success (0 < N < len(items)
// errored) and total success (0 errors) are set as Completed. // 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 type opStatus int
//go:generate stringer -type=opStatus -linecomment //go:generate stringer -type=opStatus -linecomment
@ -28,6 +32,7 @@ const (
InProgress // In Progress InProgress // In Progress
Completed // Completed Completed // Completed
Failed // Failed Failed // Failed
NoData // No Data
) )
// -------------------------------------------------------------------------------- // --------------------------------------------------------------------------------

View File

@ -12,11 +12,12 @@ func _() {
_ = x[InProgress-1] _ = x[InProgress-1]
_ = x[Completed-2] _ = x[Completed-2]
_ = x[Failed-3] _ = 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 { func (i opStatus) String() string {
if i < 0 || i >= opStatus(len(_opStatus_index)-1) { if i < 0 || i >= opStatus(len(_opStatus_index)-1) {

View File

@ -195,6 +195,10 @@ func (op *RestoreOperation) persistResults(
opStats.writeErr) opStats.writeErr)
} }
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
op.Status = NoData
}
op.Results.ReadErrors = opStats.readErr op.Results.ReadErrors = opStats.readErr
op.Results.WriteErrors = opStats.writeErr op.Results.WriteErrors = opStats.writeErr

View File

@ -5,7 +5,6 @@ import (
"testing" "testing"
"time" "time"
multierror "github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -38,56 +37,83 @@ func TestRestoreOpSuite(t *testing.T) {
suite.Run(t, new(RestoreOpSuite)) 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() { func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() {
var ( var (
t = suite.T() ctx = context.Background()
ctx = context.Background()
kw = &kopia.Wrapper{} kw = &kopia.Wrapper{}
sw = &store.Wrapper{} sw = &store.Wrapper{}
acct = account.Account{} acct = account.Account{}
now = time.Now() 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) dest = control.DefaultRestoreDestination(common.SimpleDateTimeFormat)
) )
op, err := NewRestoreOperation( table := []struct {
ctx, expectStatus opStatus
control.Options{}, expectErr assert.ErrorAssertionFunc
kw, stats restoreStats
sw, }{
acct, {
"foo", expectStatus: Completed,
selectors.Selector{}, expectErr: assert.NoError,
dest, stats: restoreStats{
evmock.NewBus()) started: true,
require.NoError(t, err) 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, test.expectStatus.String(), op.Status.String(), "status")
assert.Equal(t, len(test.stats.cs), op.Results.ItemsRead, "items read")
assert.Equal(t, op.Status.String(), Completed.String(), "status") assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors")
assert.Equal(t, op.Results.ItemsRead, len(rs.cs), "items read") assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsWritten, "items written")
assert.Equal(t, op.Results.ReadErrors, rs.readErr, "read errors") assert.Equal(t, test.stats.bytesRead.NumBytes, op.Results.BytesRead, "resource owners")
assert.Equal(t, op.Results.ItemsWritten, rs.gc.Successful, "items written") assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners")
assert.Equal(t, rs.bytesRead.NumBytes, op.Results.BytesRead, "resource owners") assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors")
assert.Equal(t, rs.resourceCount, op.Results.ResourceOwners, "resource owners") assert.Equal(t, now, op.Results.StartedAt, "started at")
assert.Equal(t, op.Results.WriteErrors, rs.writeErr, "write errors") assert.Less(t, now, op.Results.CompletedAt, "completed at")
assert.Equal(t, op.Results.StartedAt, now, "started at") })
assert.Less(t, now, op.Results.CompletedAt, "completed at") }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------