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 }