From 587c239dd19b9043f85cce8c34378b9b8442f2a9 Mon Sep 17 00:00:00 2001 From: Keepers <104464746+ryanfkeepers@users.noreply.github.com> Date: Tue, 26 Jul 2022 13:35:14 -0600 Subject: [PATCH] store backup operation results in the backup manifest (#410) * store backup operation results in the backup manifest Adds backup operation metadata like the outcome statistics and selector definitions to the backup manifest entry. These additional details will appear when users call `corso backup list`. --- src/cli/print/print.go | 3 +- src/internal/connector/exchange/message.go | 6 +- .../connector/exchange/message_test.go | 24 +-- .../connector/exchange_data_collection.go | 10 +- .../mockconnector/mock_data_collection.go | 6 +- src/internal/connector/support/status.go | 8 +- src/internal/kopia/wrapper.go | 14 +- src/internal/kopia/wrapper_test.go | 8 +- src/internal/operations/backup.go | 78 +++++---- src/internal/operations/backup_test.go | 4 +- src/internal/operations/operation.go | 28 +-- src/internal/operations/opstatus_string.go | 26 +++ src/internal/operations/restore.go | 5 +- src/internal/stats/stats.go | 20 +++ src/pkg/backup/backup.go | 162 +++++------------- src/pkg/backup/backup_test.go | 62 +++++-- src/pkg/backup/details/details.go | 116 +++++++++++++ src/pkg/backup/details/details_test.go | 116 +++++++++++++ src/pkg/repository/repository.go | 3 +- src/pkg/selectors/exchange.go | 12 +- src/pkg/selectors/exchange_test.go | 18 +- src/pkg/selectors/selectors.go | 9 + src/pkg/store/backup.go | 7 +- src/pkg/store/backup_test.go | 79 +-------- src/pkg/store/mock/store_mock.go | 5 +- 25 files changed, 501 insertions(+), 328 deletions(-) create mode 100644 src/internal/operations/opstatus_string.go create mode 100644 src/internal/stats/stats.go create mode 100644 src/pkg/backup/details/details.go create mode 100644 src/pkg/backup/details/details_test.go diff --git a/src/cli/print/print.go b/src/cli/print/print.go index 16aaef1ae..6682113ab 100644 --- a/src/cli/print/print.go +++ b/src/cli/print/print.go @@ -6,6 +6,7 @@ import ( "os" "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/spf13/cobra" "github.com/tidwall/pretty" "github.com/tomlazar/table" @@ -37,7 +38,7 @@ func Backups(bs []backup.Backup) { } // Prints the entries to the terminal with stdout. -func Entries(des []backup.DetailsEntry) { +func Entries(des []details.DetailsEntry) { ps := []Printable{} for _, de := range des { ps = append(ps, de) diff --git a/src/internal/connector/exchange/message.go b/src/internal/connector/exchange/message.go index 2483d197f..5805ef89d 100644 --- a/src/internal/connector/exchange/message.go +++ b/src/internal/connector/exchange/message.go @@ -3,11 +3,11 @@ package exchange import ( "time" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/microsoftgraph/msgraph-sdk-go/models" ) -func MessageInfo(msg models.Messageable) *backup.ExchangeInfo { +func MessageInfo(msg models.Messageable) *details.ExchangeInfo { sender := "" subject := "" received := time.Time{} @@ -22,7 +22,7 @@ func MessageInfo(msg models.Messageable) *backup.ExchangeInfo { if msg.GetReceivedDateTime() != nil { received = *msg.GetReceivedDateTime() } - return &backup.ExchangeInfo{ + return &details.ExchangeInfo{ Sender: sender, Subject: subject, Received: received, diff --git a/src/internal/connector/exchange/message_test.go b/src/internal/connector/exchange/message_test.go index 18d0c08b6..d4e9f1b95 100644 --- a/src/internal/connector/exchange/message_test.go +++ b/src/internal/connector/exchange/message_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/suite" ) @@ -20,17 +20,17 @@ func TestMessageSuite(t *testing.T) { func (suite *MessageSuite) TestMessageInfo() { tests := []struct { name string - msgAndRP func() (models.Messageable, *backup.ExchangeInfo) + msgAndRP func() (models.Messageable, *details.ExchangeInfo) }{ { name: "Empty message", - msgAndRP: func() (models.Messageable, *backup.ExchangeInfo) { - return models.NewMessage(), &backup.ExchangeInfo{} + msgAndRP: func() (models.Messageable, *details.ExchangeInfo) { + return models.NewMessage(), &details.ExchangeInfo{} }, }, { name: "Just sender", - msgAndRP: func() (models.Messageable, *backup.ExchangeInfo) { + msgAndRP: func() (models.Messageable, *details.ExchangeInfo) { sender := "foo@bar.com" sr := models.NewRecipient() sea := models.NewEmailAddress() @@ -38,30 +38,30 @@ func (suite *MessageSuite) TestMessageInfo() { sea.SetAddress(&sender) sr.SetEmailAddress(sea) msg.SetSender(sr) - return msg, &backup.ExchangeInfo{Sender: sender} + return msg, &details.ExchangeInfo{Sender: sender} }, }, { name: "Just subject", - msgAndRP: func() (models.Messageable, *backup.ExchangeInfo) { + msgAndRP: func() (models.Messageable, *details.ExchangeInfo) { subject := "Hello world" msg := models.NewMessage() msg.SetSubject(&subject) - return msg, &backup.ExchangeInfo{Subject: subject} + return msg, &details.ExchangeInfo{Subject: subject} }, }, { name: "Just receivedtime", - msgAndRP: func() (models.Messageable, *backup.ExchangeInfo) { + msgAndRP: func() (models.Messageable, *details.ExchangeInfo) { now := time.Now() msg := models.NewMessage() msg.SetReceivedDateTime(&now) - return msg, &backup.ExchangeInfo{Received: now} + return msg, &details.ExchangeInfo{Received: now} }, }, { name: "All fields", - msgAndRP: func() (models.Messageable, *backup.ExchangeInfo) { + msgAndRP: func() (models.Messageable, *details.ExchangeInfo) { sender := "foo@bar.com" subject := "Hello world" now := time.Now() @@ -73,7 +73,7 @@ func (suite *MessageSuite) TestMessageInfo() { msg.SetSender(sr) msg.SetSubject(&subject) msg.SetReceivedDateTime(&now) - return msg, &backup.ExchangeInfo{Sender: sender, Subject: subject, Received: now} + return msg, &details.ExchangeInfo{Sender: sender, Subject: subject, Received: now} }, }} for _, tt := range tests { diff --git a/src/internal/connector/exchange_data_collection.go b/src/internal/connector/exchange_data_collection.go index 45633bdfa..777b777b5 100644 --- a/src/internal/connector/exchange_data_collection.go +++ b/src/internal/connector/exchange_data_collection.go @@ -5,7 +5,7 @@ import ( "io" "github.com/alcionai/corso/internal/connector/support" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" ) const ( @@ -41,7 +41,7 @@ type DataStream interface { // DataStreamInfo is used to provide service specific // information about the DataStream type DataStreamInfo interface { - Info() backup.ItemInfo + Info() details.ItemInfo } var _ DataCollection = &ExchangeDataCollection{} @@ -103,7 +103,7 @@ type ExchangeData struct { // going forward. Using []byte for now but I assume we'll have // some structured type in here (serialization to []byte can be done in `Read`) message []byte - info *backup.ExchangeInfo + info *details.ExchangeInfo } func (ed *ExchangeData) UUID() string { @@ -114,6 +114,6 @@ func (ed *ExchangeData) ToReader() io.ReadCloser { return io.NopCloser(bytes.NewReader(ed.message)) } -func (ed *ExchangeData) Info() backup.ItemInfo { - return backup.ItemInfo{Exchange: ed.info} +func (ed *ExchangeData) Info() details.ItemInfo { + return details.ItemInfo{Exchange: ed.info} } diff --git a/src/internal/connector/mockconnector/mock_data_collection.go b/src/internal/connector/mockconnector/mock_data_collection.go index 1169b15cc..e90b57432 100644 --- a/src/internal/connector/mockconnector/mock_data_collection.go +++ b/src/internal/connector/mockconnector/mock_data_collection.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" "github.com/alcionai/corso/internal/connector" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" ) // MockExchangeDataCollection represents a mock exchange mailbox @@ -79,6 +79,6 @@ func (med *MockExchangeData) ToReader() io.ReadCloser { return med.Reader } -func (med *MockExchangeData) Info() backup.ItemInfo { - return backup.ItemInfo{Exchange: &backup.ExchangeInfo{Sender: "foo@bar.com", Subject: "Hello world!", Received: time.Now()}} +func (med *MockExchangeData) Info() details.ItemInfo { + return details.ItemInfo{Exchange: &details.ExchangeInfo{Sender: "foo@bar.com", Subject: "Hello world!", Received: time.Now()}} } diff --git a/src/internal/connector/support/status.go b/src/internal/connector/support/status.go index fbb1b338b..798214cf4 100644 --- a/src/internal/connector/support/status.go +++ b/src/internal/connector/support/status.go @@ -11,7 +11,7 @@ type ConnectorOperationStatus struct { lastOperation Operation ObjectCount int folderCount int - successful int + Successful int errorCount int incomplete bool incompleteReason string @@ -38,12 +38,12 @@ func CreateStatus(ctx context.Context, op Operation, objects, success, folders i lastOperation: op, ObjectCount: objects, folderCount: folders, - successful: success, + Successful: success, errorCount: numErr, incomplete: hasErrors, incompleteReason: reason, } - if status.ObjectCount != status.errorCount+status.successful { + if status.ObjectCount != status.errorCount+status.Successful { logger.Ctx(ctx).DPanicw( "status object count does not match errors + successes", "objects", objects, @@ -55,7 +55,7 @@ func CreateStatus(ctx context.Context, op Operation, objects, success, folders i func (cos *ConnectorOperationStatus) String() string { message := fmt.Sprintf("Action: %s performed on %d of %d objects within %d directories.", cos.lastOperation.String(), - cos.successful, cos.ObjectCount, cos.folderCount) + cos.Successful, cos.ObjectCount, cos.folderCount) if cos.incomplete { message += " " + cos.incompleteReason } diff --git a/src/internal/kopia/wrapper.go b/src/internal/kopia/wrapper.go index 1a6228247..a33eb148e 100644 --- a/src/internal/kopia/wrapper.go +++ b/src/internal/kopia/wrapper.go @@ -15,7 +15,7 @@ import ( "github.com/pkg/errors" "github.com/alcionai/corso/internal/connector" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/alcionai/corso/pkg/logger" ) @@ -81,7 +81,7 @@ func (w *Wrapper) Close(ctx context.Context) error { // DataCollection. func getStreamItemFunc( collection connector.DataCollection, - details *backup.Details, + details *details.Details, ) func(context.Context, func(context.Context, fs.Entry) error) error { return func(ctx context.Context, cb func(context.Context, fs.Entry) error) error { items := collection.Items() @@ -114,7 +114,7 @@ func getStreamItemFunc( // buildKopiaDirs recursively builds a directory hierarchy from the roots up. // Returned directories are either virtualfs.StreamingDirectory or // virtualfs.staticDirectory. -func buildKopiaDirs(dirName string, dir *treeMap, details *backup.Details) (fs.Directory, error) { +func buildKopiaDirs(dirName string, dir *treeMap, details *details.Details) (fs.Directory, error) { // Don't support directories that have both a DataCollection and a set of // static child directories. if dir.collection != nil && len(dir.childDirs) > 0 { @@ -156,7 +156,7 @@ func newTreeMap() *treeMap { // ancestor of the streams and uses virtualfs.StaticDirectory for internal nodes // in the hierarchy. Leaf nodes are virtualfs.StreamingDirectory with the given // DataCollections. -func inflateDirTree(ctx context.Context, collections []connector.DataCollection, details *backup.Details) (fs.Directory, error) { +func inflateDirTree(ctx context.Context, collections []connector.DataCollection, details *details.Details) (fs.Directory, error) { roots := make(map[string]*treeMap) for _, s := range collections { @@ -229,12 +229,12 @@ func inflateDirTree(ctx context.Context, collections []connector.DataCollection, func (w Wrapper) BackupCollections( ctx context.Context, collections []connector.DataCollection, -) (*BackupStats, *backup.Details, error) { +) (*BackupStats, *details.Details, error) { if w.c == nil { return nil, nil, errNotConnected } - details := &backup.Details{} + details := &details.Details{} dirTree, err := inflateDirTree(ctx, collections, details) if err != nil { @@ -252,7 +252,7 @@ func (w Wrapper) BackupCollections( func (w Wrapper) makeSnapshotWithRoot( ctx context.Context, root fs.Directory, - details *backup.Details, + details *details.Details, ) (*BackupStats, error) { si := snapshot.SourceInfo{ Host: corsoHost, diff --git a/src/internal/kopia/wrapper_test.go b/src/internal/kopia/wrapper_test.go index 452078fdd..501bcca81 100644 --- a/src/internal/kopia/wrapper_test.go +++ b/src/internal/kopia/wrapper_test.go @@ -20,7 +20,7 @@ import ( "github.com/alcionai/corso/internal/connector/mockconnector" "github.com/alcionai/corso/internal/kopia/mockkopia" ctesting "github.com/alcionai/corso/internal/testing" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" ) const ( @@ -117,7 +117,7 @@ func (suite *KopiaUnitSuite) TestBuildDirectoryTree() { user2: 42, } - details := &backup.Details{} + details := &details.Details{} collections := []connector.DataCollection{ mockconnector.NewMockExchangeDataCollection( @@ -180,7 +180,7 @@ func (suite *KopiaUnitSuite) TestBuildDirectoryTree_NoAncestorDirs() { expectedFileCount := 42 - details := &backup.Details{} + details := &details.Details{} collections := []connector.DataCollection{ mockconnector.NewMockExchangeDataCollection( []string{emails}, @@ -259,7 +259,7 @@ func (suite *KopiaUnitSuite) TestBuildDirectoryTree_Fails() { ctx := context.Background() suite.T().Run(test.name, func(t *testing.T) { - details := &backup.Details{} + details := &details.Details{} _, err := inflateDirTree(ctx, test.layout, details) assert.Error(t, err) }) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index c6cb2dd5f..9571acaf7 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -10,8 +10,10 @@ import ( "github.com/alcionai/corso/internal/connector/support" "github.com/alcionai/corso/internal/kopia" "github.com/alcionai/corso/internal/model" + "github.com/alcionai/corso/internal/stats" "github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/alcionai/corso/pkg/selectors" "github.com/alcionai/corso/pkg/store" ) @@ -29,8 +31,8 @@ type BackupOperation struct { // BackupResults aggregate the details of the result of the operation. type BackupResults struct { - summary - metrics + stats.ReadWrites + stats.StartAndEndTime BackupID model.ID `json:"backupID"` } @@ -71,12 +73,23 @@ type backupStats struct { } // Run begins a synchronous backup operation. -func (op *BackupOperation) Run(ctx context.Context) error { +func (op *BackupOperation) Run(ctx context.Context) (err error) { // TODO: persist initial state of backupOperation in modelstore // persist operation results to the model store on exit - stats := backupStats{} - defer op.persistResults(time.Now(), &stats) + var ( + stats backupStats + details *details.Details + ) + defer func() { + op.persistResults(time.Now(), &stats) + + err = op.createBackupModels(ctx, stats.k.SnapshotID, details) + if err != nil { + stats.writeErr = err + // todo: ^ we're not persisting this yet, except for the error shown to the user. + } + }() // retrieve data from the producer gc, err := connector.NewGraphConnector(op.account) @@ -93,7 +106,6 @@ func (op *BackupOperation) Run(ctx context.Context) error { } // hand the results to the consumer - var details *backup.Details stats.k, details, err = op.kopia.BackupCollections(ctx, cs) if err != nil { stats.writeErr = err @@ -101,33 +113,11 @@ func (op *BackupOperation) Run(ctx context.Context) error { } stats.gc = gc.AwaitStatus() - err = op.createBackupModels(ctx, stats.k.SnapshotID, details) - if err != nil { - stats.writeErr = err - return err - } - return nil + return err } -func (op *BackupOperation) createBackupModels(ctx context.Context, snapID string, details *backup.Details) error { - err := op.store.Put(ctx, model.BackupDetailsSchema, &details.DetailsModel) - if err != nil { - return errors.Wrap(err, "creating backupdetails model") - } - - bu := backup.New(snapID, string(details.ModelStoreID)) - - err = op.store.Put(ctx, model.BackupSchema, bu) - if err != nil { - return errors.Wrap(err, "creating backup model") - } - - op.Results.BackupID = bu.StableID - - return nil -} - -// writes the backupOperation outcome to the modelStore. +// writes the results metrics to the operation results. +// later stored in the manifest using createBackupModels. func (op *BackupOperation) persistResults( started time.Time, stats *backupStats, @@ -141,7 +131,7 @@ func (op *BackupOperation) persistResults( op.Results.WriteErrors = stats.writeErr if stats.gc != nil { - op.Results.ItemsRead = stats.gc.ObjectCount + op.Results.ItemsRead = stats.gc.Successful } if stats.k != nil { op.Results.ItemsWritten = stats.k.TotalFileCount @@ -149,6 +139,26 @@ func (op *BackupOperation) persistResults( op.Results.StartedAt = started op.Results.CompletedAt = time.Now() - - // TODO: persist operation to modelstore +} + +// stores the operation details, results, and selectors in the backup manifest. +func (op *BackupOperation) createBackupModels(ctx context.Context, snapID string, details *details.Details) error { + err := op.store.Put(ctx, model.BackupDetailsSchema, &details.DetailsModel) + if err != nil { + return errors.Wrap(err, "creating backupdetails model") + } + + b := backup.New( + snapID, string(details.ModelStoreID), op.Status.String(), + op.Selectors, + op.Results.ReadWrites, + op.Results.StartAndEndTime, + ) + err = op.store.Put(ctx, model.BackupSchema, b) + if err != nil { + return errors.Wrap(err, "creating backup model") + } + op.Results.BackupID = b.StableID + + return nil } diff --git a/src/internal/operations/backup_test.go b/src/internal/operations/backup_test.go index 4a100544f..5bfcfdf44 100644 --- a/src/internal/operations/backup_test.go +++ b/src/internal/operations/backup_test.go @@ -48,7 +48,7 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { TotalFileCount: 1, }, gc: &support.ConnectorOperationStatus{ - ObjectCount: 1, + Successful: 1, }, } ) @@ -59,7 +59,7 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { op.persistResults(now, &stats) assert.Equal(t, op.Status, Failed) - assert.Equal(t, op.Results.ItemsRead, stats.gc.ObjectCount) + assert.Equal(t, op.Results.ItemsRead, stats.gc.Successful) assert.Equal(t, op.Results.ReadErrors, stats.readErr) assert.Equal(t, op.Results.ItemsWritten, stats.k.TotalFileCount) assert.Equal(t, op.Results.WriteErrors, stats.writeErr) diff --git a/src/internal/operations/operation.go b/src/internal/operations/operation.go index 3153378ad..78dd415c7 100644 --- a/src/internal/operations/operation.go +++ b/src/internal/operations/operation.go @@ -12,11 +12,12 @@ import ( type opStatus int +//go:generate stringer -type=opStatus -linecomment const ( - Unknown opStatus = iota - InProgress - Successful - Failed + Unknown opStatus = iota // Status Unknown + InProgress // In Progress + Successful // Successful + Failed // Failed ) // -------------------------------------------------------------------------------- @@ -72,22 +73,3 @@ func (op operation) validate() error { } return nil } - -// -------------------------------------------------------------------------------- -// Results -// -------------------------------------------------------------------------------- - -// Summary tracks the total files touched and errors produced -// during an operation. -type summary struct { - ItemsRead int `json:"itemsRead,omitempty"` - ItemsWritten int `json:"itemsWritten,omitempty"` - ReadErrors error `json:"readErrors,omitempty"` - WriteErrors error `json:"writeErrors,omitempty"` -} - -// Metrics tracks performance details such as timing, throughput, etc. -type metrics struct { - StartedAt time.Time `json:"startedAt"` - CompletedAt time.Time `json:"completedAt"` -} diff --git a/src/internal/operations/opstatus_string.go b/src/internal/operations/opstatus_string.go new file mode 100644 index 000000000..4b198783f --- /dev/null +++ b/src/internal/operations/opstatus_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type=opStatus -linecomment"; DO NOT EDIT. + +package operations + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Unknown-0] + _ = x[InProgress-1] + _ = x[Successful-2] + _ = x[Failed-3] +} + +const _opStatus_name = "Status UnknownIn ProgressSuccessfulFailed" + +var _opStatus_index = [...]uint8{0, 14, 25, 35, 41} + +func (i opStatus) String() string { + if i < 0 || i >= opStatus(len(_opStatus_index)-1) { + return "opStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _opStatus_name[_opStatus_index[i]:_opStatus_index[i+1]] +} diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index fcfb6bacc..1c4ed4e25 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -11,6 +11,7 @@ import ( "github.com/alcionai/corso/internal/connector/support" "github.com/alcionai/corso/internal/kopia" "github.com/alcionai/corso/internal/model" + "github.com/alcionai/corso/internal/stats" "github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/selectors" "github.com/alcionai/corso/pkg/store" @@ -30,8 +31,8 @@ type RestoreOperation struct { // RestoreResults aggregate the details of the results of the operation. type RestoreResults struct { - summary - metrics + stats.ReadWrites + stats.StartAndEndTime } // NewRestoreOperation constructs and validates a restore operation. diff --git a/src/internal/stats/stats.go b/src/internal/stats/stats.go new file mode 100644 index 000000000..60ffb16ab --- /dev/null +++ b/src/internal/stats/stats.go @@ -0,0 +1,20 @@ +package stats + +import "time" + +// ReadWrites tracks the total count of reads and writes, and of +// read and write errors. ItemsRead and ItemsWritten counts are +// assumed to be successful, so the total count of items involved +// would be ItemsRead+ReadErrors. +type ReadWrites struct { + ItemsRead int `json:"itemsRead,omitempty"` + ItemsWritten int `json:"itemsWritten,omitempty"` + ReadErrors error `json:"readErrors,omitempty"` + WriteErrors error `json:"writeErrors,omitempty"` +} + +// StartAndEndTime tracks a paired starting time and ending time. +type StartAndEndTime struct { + StartedAt time.Time `json:"startedAt"` + CompletedAt time.Time `json:"completedAt"` +} diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index d16e93af7..63e643b49 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -1,10 +1,14 @@ package backup import ( - "sync" + "strconv" "time" + "github.com/alcionai/corso/internal/common" + "github.com/alcionai/corso/internal/connector/support" "github.com/alcionai/corso/internal/model" + "github.com/alcionai/corso/internal/stats" + "github.com/alcionai/corso/pkg/selectors" ) // Backup represents the result of a backup operation @@ -17,10 +21,17 @@ type Backup struct { // Reference to `Details` // We store the ModelStoreID since Details is immutable - DetailsID string `json:"detailsId"` + DetailsID string `json:"detailsID"` - // TODO: - // - Backup "Specification" + // Status of the operation + Status string `json:"status"` + + // Selectors used in this operation + Selectors selectors.Selector `json:"selectors"` + + // stats are embedded so that the values appear as top-level properties + stats.ReadWrites + stats.StartAndEndTime } // Headers returns the human-readable names of properties in a Backup @@ -31,6 +42,14 @@ func (b Backup) Headers() []string { "Stable ID", "Snapshot ID", "Details ID", + "Status", + "Selectors", + "Items Read", + "Items Written", + "Read Errors", + "Write Errors", + "Started At", + "Completed At", } } @@ -38,125 +57,34 @@ func (b Backup) Headers() []string { // out to a terminal in a columnar display. func (b Backup) Values() []string { return []string{ - b.CreationTime.Format(time.RFC3339Nano), + common.FormatTime(b.CreationTime), string(b.StableID), b.SnapshotID, b.DetailsID, + b.Status, + b.Selectors.String(), + strconv.Itoa(b.ReadWrites.ItemsRead), + strconv.Itoa(b.ReadWrites.ItemsWritten), + strconv.Itoa(support.GetNumberOfErrors(b.ReadWrites.ReadErrors)), + strconv.Itoa(support.GetNumberOfErrors(b.ReadWrites.WriteErrors)), + common.FormatTime(b.StartAndEndTime.StartedAt), + common.FormatTime(b.StartAndEndTime.CompletedAt), } } -func New(snapshotID, detailsID string) *Backup { +func New( + snapshotID, detailsID, status string, + selector selectors.Selector, + rw stats.ReadWrites, + se stats.StartAndEndTime, +) *Backup { return &Backup{ - CreationTime: time.Now(), - SnapshotID: snapshotID, - DetailsID: detailsID, + CreationTime: time.Now(), + SnapshotID: snapshotID, + DetailsID: detailsID, + Status: status, + Selectors: selector, + ReadWrites: rw, + StartAndEndTime: se, } } - -// DetailsModel describes what was stored in a Backup -type DetailsModel struct { - model.BaseModel - Entries []DetailsEntry `json:"entries"` -} - -// Details augments the core with a mutex for processing. -// Should be sliced back to d.DetailsModel for storage and -// printing. -type Details struct { - DetailsModel - - // internal - mu sync.Mutex `json:"-"` -} - -// DetailsEntry describes a single item stored in a Backup -type DetailsEntry struct { - // TODO: `RepoRef` is currently the full path to the item in Kopia - // This can be optimized. - RepoRef string `json:"repoRef"` - ItemInfo -} - -// Paths returns the list of Paths extracted from the Entries slice. -func (dm DetailsModel) Paths() []string { - ents := dm.Entries - r := make([]string, len(ents)) - for i := range ents { - r[i] = ents[i].RepoRef - } - return r -} - -// Headers returns the human-readable names of properties in a DetailsEntry -// for printing out to a terminal in a columnar display. -func (de DetailsEntry) Headers() []string { - hs := []string{"Repo Ref"} - if de.ItemInfo.Exchange != nil { - hs = append(hs, de.ItemInfo.Exchange.Headers()...) - } - if de.ItemInfo.Sharepoint != nil { - hs = append(hs, de.ItemInfo.Sharepoint.Headers()...) - } - return hs -} - -// Values returns the values matching the Headers list. -func (de DetailsEntry) Values() []string { - vs := []string{de.RepoRef} - if de.ItemInfo.Exchange != nil { - vs = append(vs, de.ItemInfo.Exchange.Values()...) - } - if de.ItemInfo.Sharepoint != nil { - vs = append(vs, de.ItemInfo.Sharepoint.Values()...) - } - return vs -} - -// ItemInfo is a oneOf that contains service specific -// information about the item it tracks -type ItemInfo struct { - Exchange *ExchangeInfo `json:"exchange,omitempty"` - Sharepoint *SharepointInfo `json:"sharepoint,omitempty"` -} - -// ExchangeInfo describes an exchange item -type ExchangeInfo struct { - Sender string `json:"sender"` - Subject string `json:"subject"` - Received time.Time `json:"received"` -} - -// Headers returns the human-readable names of properties in an ExchangeInfo -// for printing out to a terminal in a columnar display. -func (e ExchangeInfo) Headers() []string { - return []string{"Sender", "Subject", "Received"} -} - -// Values returns the values matching the Headers list for printing -// out to a terminal in a columnar display. -func (e ExchangeInfo) Values() []string { - return []string{e.Sender, e.Subject, e.Received.Format(time.RFC3339Nano)} -} - -// SharepointInfo describes a sharepoint item -// TODO: Implement this. This is currently here -// just to illustrate usage -type SharepointInfo struct{} - -func (d *Details) Add(repoRef string, info ItemInfo) { - d.mu.Lock() - defer d.mu.Unlock() - d.Entries = append(d.Entries, DetailsEntry{RepoRef: repoRef, ItemInfo: info}) -} - -// Headers returns the human-readable names of properties in a SharepointInfo -// for printing out to a terminal in a columnar display. -func (s SharepointInfo) Headers() []string { - return []string{} -} - -// Values returns the values matching the Headers list for printing -// out to a terminal in a columnar display. -func (s SharepointInfo) Values() []string { - return []string{} -} diff --git a/src/pkg/backup/backup_test.go b/src/pkg/backup/backup_test.go index 69b5fb0b7..e4bc47bd4 100644 --- a/src/pkg/backup/backup_test.go +++ b/src/pkg/backup/backup_test.go @@ -1,14 +1,19 @@ package backup_test import ( + "errors" "testing" "time" "github.com/stretchr/testify/suite" "github.com/zeebo/assert" + "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/internal/model" + "github.com/alcionai/corso/internal/stats" "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" + "github.com/alcionai/corso/pkg/selectors" ) type BackupSuite struct { @@ -30,6 +35,18 @@ func (suite *BackupSuite) TestBackup_HeadersValues() { CreationTime: now, SnapshotID: "snapshot", DetailsID: "details", + Status: "status", + Selectors: selectors.Selector{}, + ReadWrites: stats.ReadWrites{ + ItemsRead: 1, + ItemsWritten: 1, + ReadErrors: errors.New("1"), + WriteErrors: errors.New("1"), + }, + StartAndEndTime: stats.StartAndEndTime{ + StartedAt: now, + CompletedAt: now, + }, } expectHs := []string{ @@ -37,15 +54,32 @@ func (suite *BackupSuite) TestBackup_HeadersValues() { "Stable ID", "Snapshot ID", "Details ID", + "Status", + "Selectors", + "Items Read", + "Items Written", + "Read Errors", + "Write Errors", + "Started At", + "Completed At", } hs := b.Headers() assert.DeepEqual(t, expectHs, hs) + nowFmt := common.FormatTime(now) expectVs := []string{ - now.Format(time.RFC3339Nano), + nowFmt, "stable", "snapshot", "details", + "status", + "{}", + "1", + "1", + "1", + "1", + nowFmt, + nowFmt, } vs := b.Values() assert.DeepEqual(t, expectVs, vs) @@ -57,13 +91,13 @@ func (suite *BackupSuite) TestDetailsEntry_HeadersValues() { table := []struct { name string - entry backup.DetailsEntry + entry details.DetailsEntry expectHs []string expectVs []string }{ { name: "no info", - entry: backup.DetailsEntry{ + entry: details.DetailsEntry{ RepoRef: "reporef", }, expectHs: []string{"Repo Ref"}, @@ -71,10 +105,10 @@ func (suite *BackupSuite) TestDetailsEntry_HeadersValues() { }, { name: "exhange info", - entry: backup.DetailsEntry{ + entry: details.DetailsEntry{ RepoRef: "reporef", - ItemInfo: backup.ItemInfo{ - Exchange: &backup.ExchangeInfo{ + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ Sender: "sender", Subject: "subject", Received: now, @@ -86,10 +120,10 @@ func (suite *BackupSuite) TestDetailsEntry_HeadersValues() { }, { name: "sharepoint info", - entry: backup.DetailsEntry{ + entry: details.DetailsEntry{ RepoRef: "reporef", - ItemInfo: backup.ItemInfo{ - Sharepoint: &backup.SharepointInfo{}, + ItemInfo: details.ItemInfo{ + Sharepoint: &details.SharepointInfo{}, }, }, expectHs: []string{"Repo Ref"}, @@ -110,7 +144,7 @@ func (suite *BackupSuite) TestDetailsEntry_HeadersValues() { func (suite *BackupSuite) TestDetailsModel_Path() { table := []struct { name string - ents []backup.DetailsEntry + ents []details.DetailsEntry expect []string }{ { @@ -120,14 +154,14 @@ func (suite *BackupSuite) TestDetailsModel_Path() { }, { name: "single entry", - ents: []backup.DetailsEntry{ + ents: []details.DetailsEntry{ {RepoRef: "abcde"}, }, expect: []string{"abcde"}, }, { name: "multiple entries", - ents: []backup.DetailsEntry{ + ents: []details.DetailsEntry{ {RepoRef: "abcde"}, {RepoRef: "12345"}, }, @@ -136,8 +170,8 @@ func (suite *BackupSuite) TestDetailsModel_Path() { } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - d := backup.Details{ - DetailsModel: backup.DetailsModel{ + d := details.Details{ + DetailsModel: details.DetailsModel{ Entries: test.ents, }, } diff --git a/src/pkg/backup/details/details.go b/src/pkg/backup/details/details.go new file mode 100644 index 000000000..7a1b6ae80 --- /dev/null +++ b/src/pkg/backup/details/details.go @@ -0,0 +1,116 @@ +package details + +import ( + "sync" + "time" + + "github.com/alcionai/corso/internal/model" +) + +// DetailsModel describes what was stored in a Backup +type DetailsModel struct { + model.BaseModel + Entries []DetailsEntry `json:"entries"` +} + +// Details augments the core with a mutex for processing. +// Should be sliced back to d.DetailsModel for storage and +// printing. +type Details struct { + DetailsModel + + // internal + mu sync.Mutex `json:"-"` +} + +// DetailsEntry describes a single item stored in a Backup +type DetailsEntry struct { + // TODO: `RepoRef` is currently the full path to the item in Kopia + // This can be optimized. + RepoRef string `json:"repoRef"` + ItemInfo +} + +// Paths returns the list of Paths extracted from the Entries slice. +func (dm DetailsModel) Paths() []string { + ents := dm.Entries + r := make([]string, len(ents)) + for i := range ents { + r[i] = ents[i].RepoRef + } + return r +} + +// Headers returns the human-readable names of properties in a DetailsEntry +// for printing out to a terminal in a columnar display. +func (de DetailsEntry) Headers() []string { + hs := []string{"Repo Ref"} + if de.ItemInfo.Exchange != nil { + hs = append(hs, de.ItemInfo.Exchange.Headers()...) + } + if de.ItemInfo.Sharepoint != nil { + hs = append(hs, de.ItemInfo.Sharepoint.Headers()...) + } + return hs +} + +// Values returns the values matching the Headers list. +func (de DetailsEntry) Values() []string { + vs := []string{de.RepoRef} + if de.ItemInfo.Exchange != nil { + vs = append(vs, de.ItemInfo.Exchange.Values()...) + } + if de.ItemInfo.Sharepoint != nil { + vs = append(vs, de.ItemInfo.Sharepoint.Values()...) + } + return vs +} + +// ItemInfo is a oneOf that contains service specific +// information about the item it tracks +type ItemInfo struct { + Exchange *ExchangeInfo `json:"exchange,omitempty"` + Sharepoint *SharepointInfo `json:"sharepoint,omitempty"` +} + +// ExchangeInfo describes an exchange item +type ExchangeInfo struct { + Sender string `json:"sender"` + Subject string `json:"subject"` + Received time.Time `json:"received"` +} + +// Headers returns the human-readable names of properties in an ExchangeInfo +// for printing out to a terminal in a columnar display. +func (e ExchangeInfo) Headers() []string { + return []string{"Sender", "Subject", "Received"} +} + +// Values returns the values matching the Headers list for printing +// out to a terminal in a columnar display. +func (e ExchangeInfo) Values() []string { + return []string{e.Sender, e.Subject, e.Received.Format(time.RFC3339Nano)} +} + +// SharepointInfo describes a sharepoint item +// TODO: Implement this. This is currently here +// just to illustrate usage +type SharepointInfo struct{} + +func (d *Details) Add(repoRef string, info ItemInfo) { + d.mu.Lock() + defer d.mu.Unlock() + d.Entries = append(d.Entries, DetailsEntry{RepoRef: repoRef, ItemInfo: info}) +} + +// Headers returns the human-readable names of properties in a SharepointInfo +// for printing out to a terminal in a columnar display. +func (s SharepointInfo) Headers() []string { + return []string{} +} + +// Values returns the values matching the Headers list for printing +// out to a terminal in a columnar display. +func (s SharepointInfo) Values() []string { + return []string{} +} diff --git a/src/pkg/backup/details/details_test.go b/src/pkg/backup/details/details_test.go new file mode 100644 index 000000000..a09e52a79 --- /dev/null +++ b/src/pkg/backup/details/details_test.go @@ -0,0 +1,116 @@ +package details_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/kopia/kopia/repo/manifest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/internal/model" + "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" + "github.com/alcionai/corso/pkg/store" + storeMock "github.com/alcionai/corso/pkg/store/mock" +) + +// ------------------------------------------------------------ +// unit tests +// ------------------------------------------------------------ + +var ( + detailsID = uuid.NewString() + bu = backup.Backup{ + BaseModel: model.BaseModel{ + StableID: model.ID(uuid.NewString()), + ModelStoreID: manifest.ID(uuid.NewString()), + }, + CreationTime: time.Now(), + SnapshotID: uuid.NewString(), + DetailsID: detailsID, + } + deets = details.Details{ + DetailsModel: details.DetailsModel{ + BaseModel: model.BaseModel{ + StableID: model.ID(detailsID), + ModelStoreID: manifest.ID(uuid.NewString()), + }, + }, + } +) + +type StoreDetailsUnitSuite struct { + suite.Suite +} + +func TestStoreDetailsUnitSuite(t *testing.T) { + suite.Run(t, new(StoreDetailsUnitSuite)) +} + +func (suite *StoreDetailsUnitSuite) TestGetDetails() { + ctx := context.Background() + + table := []struct { + name string + mock *storeMock.MockModelStore + expect assert.ErrorAssertionFunc + }{ + { + name: "gets details", + mock: storeMock.NewMock(nil, &deets, nil), + expect: assert.NoError, + }, + { + name: "errors", + mock: storeMock.NewMock(nil, &deets, assert.AnError), + expect: assert.Error, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sm := &store.Wrapper{Storer: test.mock} + result, err := sm.GetDetails(ctx, manifest.ID(uuid.NewString())) + test.expect(t, err) + if err != nil { + return + } + assert.Equal(t, deets.StableID, result.StableID) + }) + } +} + +func (suite *StoreDetailsUnitSuite) TestGetDetailsFromBackupID() { + ctx := context.Background() + + table := []struct { + name string + mock *storeMock.MockModelStore + expect assert.ErrorAssertionFunc + }{ + { + name: "gets details from backup id", + mock: storeMock.NewMock(&bu, &deets, nil), + expect: assert.NoError, + }, + { + name: "errors", + mock: storeMock.NewMock(&bu, &deets, assert.AnError), + expect: assert.Error, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + store := &store.Wrapper{Storer: test.mock} + dResult, bResult, err := store.GetDetailsFromBackupID(ctx, model.ID(uuid.NewString())) + test.expect(t, err) + if err != nil { + return + } + assert.Equal(t, deets.StableID, dResult.StableID) + assert.Equal(t, bu.StableID, bResult.StableID) + }) + } +} diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index e03f64cba..0dfdcee33 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -12,6 +12,7 @@ import ( "github.com/alcionai/corso/internal/operations" "github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/alcionai/corso/pkg/selectors" "github.com/alcionai/corso/pkg/storage" "github.com/alcionai/corso/pkg/store" @@ -162,7 +163,7 @@ func (r Repository) Backups(ctx context.Context) ([]backup.Backup, error) { } // BackupDetails returns the specified backup details object -func (r Repository) BackupDetails(ctx context.Context, backupID string) (*backup.Details, *backup.Backup, error) { +func (r Repository) BackupDetails(ctx context.Context, backupID string) (*details.Details, *backup.Backup, error) { sw := store.NewKopiaStore(r.modelStore) return sw.GetDetailsFromBackupID(ctx, model.ID(backupID)) } diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index 24bb34cd4..ad4a26a44 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -4,7 +4,7 @@ import ( "strings" "github.com/alcionai/corso/internal/common" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" ) // --------------------------------------------------------------------------- @@ -500,13 +500,13 @@ var categoryPathSet = map[exchangeCategory][]exchangeCategory{ } // matches returns true if either the path or the info matches the scope details. -func (s ExchangeScope) matches(cat exchangeCategory, path []string, info *backup.ExchangeInfo) bool { +func (s ExchangeScope) matches(cat exchangeCategory, path []string, info *details.ExchangeInfo) bool { return s.matchesPath(cat, path) || s.matchesInfo(cat, info) } // matchesInfo handles the standard behavior when comparing a scope and an exchangeInfo // returns true if the scope and info match for the provided category. -func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *backup.ExchangeInfo) bool { +func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeInfo) bool { // we need values to match against if info == nil { return false @@ -634,7 +634,7 @@ func exchangeIDPath(cat exchangeCategory, path []string) map[exchangeCategory]st // Reduce reduces the entries in a backupDetails struct to only // those that match the inclusions, filters, and exclusions in the selector. -func (s *ExchangeRestore) Reduce(deets *backup.Details) *backup.Details { +func (s *ExchangeRestore) Reduce(deets *details.Details) *details.Details { if deets == nil { return nil } @@ -643,7 +643,7 @@ func (s *ExchangeRestore) Reduce(deets *backup.Details) *backup.Details { entFilt := exchangeScopesByCategory(s.Filters) entIncs := exchangeScopesByCategory(s.Includes) - ents := []backup.DetailsEntry{} + ents := []details.DetailsEntry{} for _, ent := range deets.Entries { // todo: use Path pkg for this @@ -706,7 +706,7 @@ func exchangeScopesByCategory(scopes []map[string]string) map[string][]ExchangeS func matchExchangeEntry( cat exchangeCategory, path []string, - info *backup.ExchangeInfo, + info *details.ExchangeInfo, excs, filts, incs []ExchangeScope, ) bool { // a passing match requires either a filter or an inclusion diff --git a/src/pkg/selectors/exchange_test.go b/src/pkg/selectors/exchange_test.go index 167a8705a..4d5a72112 100644 --- a/src/pkg/selectors/exchange_test.go +++ b/src/pkg/selectors/exchange_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/alcionai/corso/internal/common" - "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -498,7 +498,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_MatchesInfo() { epoch = time.Time{} now = time.Now() then = now.Add(1 * time.Minute) - info = &backup.ExchangeInfo{ + info = &details.ExchangeInfo{ Sender: sender, Subject: subject, Received: now, @@ -629,14 +629,14 @@ func (suite *ExchangeSourceSuite) TestIdPath() { } func (suite *ExchangeSourceSuite) TestExchangeRestore_Reduce() { - makeDeets := func(refs ...string) *backup.Details { - deets := &backup.Details{ - DetailsModel: backup.DetailsModel{ - Entries: []backup.DetailsEntry{}, + makeDeets := func(refs ...string) *details.Details { + deets := &details.Details{ + DetailsModel: details.DetailsModel{ + Entries: []details.DetailsEntry{}, }, } for _, r := range refs { - deets.Entries = append(deets.Entries, backup.DetailsEntry{ + deets.Entries = append(deets.Entries, details.DetailsEntry{ RepoRef: r, }) } @@ -652,7 +652,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_Reduce() { } table := []struct { name string - deets *backup.Details + deets *details.Details makeSelector func() *ExchangeRestore expect []string }{ @@ -825,7 +825,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() { } func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() { - var exchangeInfo *backup.ExchangeInfo + var exchangeInfo *details.ExchangeInfo const ( mid = "mailID" cat = ExchangeMail diff --git a/src/pkg/selectors/selectors.go b/src/pkg/selectors/selectors.go index 510b523c5..aab4c2ec4 100644 --- a/src/pkg/selectors/selectors.go +++ b/src/pkg/selectors/selectors.go @@ -1,6 +1,7 @@ package selectors import ( + "encoding/json" "strings" "github.com/pkg/errors" @@ -84,6 +85,14 @@ func None() []string { return []string{NoneTgt} } +func (s Selector) String() string { + bs, err := json.Marshal(s) + if err != nil { + return "error" + } + return string(bs) +} + type baseScope interface { ~map[string]string } diff --git a/src/pkg/store/backup.go b/src/pkg/store/backup.go index 9696cad48..8d427c4c0 100644 --- a/src/pkg/store/backup.go +++ b/src/pkg/store/backup.go @@ -8,6 +8,7 @@ import ( "github.com/alcionai/corso/internal/model" "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" ) // GetBackup gets a single backup by id. @@ -39,8 +40,8 @@ func (w Wrapper) GetBackups(ctx context.Context) ([]backup.Backup, error) { } // GetDetails gets the backup details by ID. -func (w Wrapper) GetDetails(ctx context.Context, detailsID manifest.ID) (*backup.Details, error) { - d := backup.Details{} +func (w Wrapper) GetDetails(ctx context.Context, detailsID manifest.ID) (*details.Details, error) { + d := details.Details{} err := w.GetWithModelStoreID(ctx, model.BackupDetailsSchema, detailsID, &d) if err != nil { return nil, errors.Wrap(err, "getting details") @@ -49,7 +50,7 @@ func (w Wrapper) GetDetails(ctx context.Context, detailsID manifest.ID) (*backup } // GetDetailsFromBackupID retrieves the backup.Details within the specified backup. -func (w Wrapper) GetDetailsFromBackupID(ctx context.Context, backupID model.ID) (*backup.Details, *backup.Backup, error) { +func (w Wrapper) GetDetailsFromBackupID(ctx context.Context, backupID model.ID) (*details.Details, *backup.Backup, error) { b, err := w.GetBackup(ctx, backupID) if err != nil { return nil, nil, err diff --git a/src/pkg/store/backup_test.go b/src/pkg/store/backup_test.go index cabec323a..08c11864a 100644 --- a/src/pkg/store/backup_test.go +++ b/src/pkg/store/backup_test.go @@ -31,14 +31,6 @@ var ( SnapshotID: uuid.NewString(), DetailsID: detailsID, } - deets = backup.Details{ - DetailsModel: backup.DetailsModel{ - BaseModel: model.BaseModel{ - StableID: model.ID(detailsID), - ModelStoreID: manifest.ID(uuid.NewString()), - }, - }, - } ) type StoreBackupUnitSuite struct { @@ -70,8 +62,8 @@ func (suite *StoreBackupUnitSuite) TestGetBackup() { } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - store := &store.Wrapper{test.mock} - result, err := store.GetBackup(ctx, model.ID(uuid.NewString())) + sm := &store.Wrapper{Storer: test.mock} + result, err := sm.GetBackup(ctx, model.ID(uuid.NewString())) test.expect(t, err) if err != nil { return @@ -102,7 +94,7 @@ func (suite *StoreBackupUnitSuite) TestGetBackups() { } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - sm := &store.Wrapper{test.mock} + sm := &store.Wrapper{Storer: test.mock} result, err := sm.GetBackups(ctx) test.expect(t, err) if err != nil { @@ -113,68 +105,3 @@ func (suite *StoreBackupUnitSuite) TestGetBackups() { }) } } - -func (suite *StoreBackupUnitSuite) TestGetDetails() { - ctx := context.Background() - - table := []struct { - name string - mock *storeMock.MockModelStore - expect assert.ErrorAssertionFunc - }{ - { - name: "gets details", - mock: storeMock.NewMock(nil, &deets, nil), - expect: assert.NoError, - }, - { - name: "errors", - mock: storeMock.NewMock(nil, &deets, assert.AnError), - expect: assert.Error, - }, - } - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - sm := &store.Wrapper{test.mock} - result, err := sm.GetDetails(ctx, manifest.ID(uuid.NewString())) - test.expect(t, err) - if err != nil { - return - } - assert.Equal(t, deets.StableID, result.StableID) - }) - } -} - -func (suite *StoreBackupUnitSuite) TestGetDetailsFromBackupID() { - ctx := context.Background() - - table := []struct { - name string - mock *storeMock.MockModelStore - expect assert.ErrorAssertionFunc - }{ - { - name: "gets details from backup id", - mock: storeMock.NewMock(&bu, &deets, nil), - expect: assert.NoError, - }, - { - name: "errors", - mock: storeMock.NewMock(&bu, &deets, assert.AnError), - expect: assert.Error, - }, - } - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - store := &store.Wrapper{test.mock} - dResult, bResult, err := store.GetDetailsFromBackupID(ctx, model.ID(uuid.NewString())) - test.expect(t, err) - if err != nil { - return - } - assert.Equal(t, deets.StableID, dResult.StableID) - assert.Equal(t, bu.StableID, bResult.StableID) - }) - } -} diff --git a/src/pkg/store/mock/store_mock.go b/src/pkg/store/mock/store_mock.go index 3f122c42b..304cc9d11 100644 --- a/src/pkg/store/mock/store_mock.go +++ b/src/pkg/store/mock/store_mock.go @@ -9,6 +9,7 @@ import ( "github.com/alcionai/corso/internal/model" "github.com/alcionai/corso/pkg/backup" + "github.com/alcionai/corso/pkg/backup/details" ) // ------------------------------------------------------------ @@ -21,7 +22,7 @@ type MockModelStore struct { err error } -func NewMock(b *backup.Backup, d *backup.Details, err error) *MockModelStore { +func NewMock(b *backup.Backup, d *details.Details, err error) *MockModelStore { return &MockModelStore{ backup: marshal(b), details: marshal(d), @@ -89,7 +90,7 @@ func (mms *MockModelStore) GetIDsForType( unmarshal(mms.backup, &b) return []*model.BaseModel{&b.BaseModel}, nil case model.BackupDetailsSchema: - d := backup.Details{} + d := details.Details{} unmarshal(mms.backup, &d) return []*model.BaseModel{&d.BaseModel}, nil }