update operation/backup errs (#2239)
## Description Begins updating operations/backup with the new error handling procedures. For backwards compatibility, errors are currently duplicated in the old stats.Errs and the new Errors struct. ## Does this PR need a docs update or release note? - [x] ⛔ No ## Type of change - [x] 🧹 Tech Debt/Cleanup ## Issue(s) * #1970 ## Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
f3b2e9a632
commit
070b8fddee
@ -144,6 +144,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
err = op.persistResults(startTime, &opStats)
|
err = op.persistResults(startTime, &opStats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
op.Errors.Fail(errors.Wrap(err, "persisting backup results"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +154,8 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
|
|||||||
opStats.k.SnapshotID,
|
opStats.k.SnapshotID,
|
||||||
backupDetails.Details())
|
backupDetails.Details())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opStats.writeErr = err
|
op.Errors.Fail(errors.Wrap(err, "persisting backup"))
|
||||||
|
opStats.writeErr = op.Errors.Err()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -164,21 +166,27 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
|
|||||||
reasons,
|
reasons,
|
||||||
tenantID,
|
tenantID,
|
||||||
uib,
|
uib,
|
||||||
)
|
op.Errors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opStats.readErr = errors.Wrap(err, "connecting to M365")
|
op.Errors.Fail(errors.Wrap(err, "collecting manifest heuristics"))
|
||||||
|
opStats.readErr = op.Errors.Err()
|
||||||
|
|
||||||
return opStats.readErr
|
return opStats.readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
gc, err := connectToM365(ctx, op.Selectors, op.account)
|
gc, err := connectToM365(ctx, op.Selectors, op.account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opStats.readErr = errors.Wrap(err, "connecting to M365")
|
op.Errors.Fail(errors.Wrap(err, "connecting to m365"))
|
||||||
|
opStats.readErr = op.Errors.Err()
|
||||||
|
|
||||||
return opStats.readErr
|
return opStats.readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
cs, err := produceBackupDataCollections(ctx, gc, op.Selectors, mdColls, op.Options)
|
cs, err := produceBackupDataCollections(ctx, gc, op.Selectors, mdColls, op.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opStats.readErr = errors.Wrap(err, "retrieving data to backup")
|
op.Errors.Fail(errors.Wrap(err, "retrieving data to backup"))
|
||||||
|
opStats.readErr = op.Errors.Err()
|
||||||
|
|
||||||
return opStats.readErr
|
return opStats.readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +202,9 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
|
|||||||
op.Results.BackupID,
|
op.Results.BackupID,
|
||||||
uib && canUseMetaData)
|
uib && canUseMetaData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opStats.writeErr = errors.Wrap(err, "backing up service data")
|
op.Errors.Fail(errors.Wrap(err, "backing up service data"))
|
||||||
|
opStats.writeErr = op.Errors.Err()
|
||||||
|
|
||||||
return opStats.writeErr
|
return opStats.writeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,12 +221,15 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
|
|||||||
toMerge,
|
toMerge,
|
||||||
backupDetails,
|
backupDetails,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
opStats.writeErr = errors.Wrap(err, "merging backup details")
|
op.Errors.Fail(errors.Wrap(err, "merging backup details"))
|
||||||
|
opStats.writeErr = op.Errors.Err()
|
||||||
|
|
||||||
return opStats.writeErr
|
return opStats.writeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
opStats.gc = gc.AwaitStatus()
|
opStats.gc = gc.AwaitStatus()
|
||||||
|
|
||||||
|
// TODO(keepers): remove when fault.Errors handles all iterable error aggregation.
|
||||||
if opStats.gc.ErrorCount > 0 {
|
if opStats.gc.ErrorCount > 0 {
|
||||||
merr := multierror.Append(opStats.readErr, errors.Wrap(opStats.gc.Err, "retrieving data"))
|
merr := multierror.Append(opStats.readErr, errors.Wrap(opStats.gc.Err, "retrieving data"))
|
||||||
opStats.readErr = merr.ErrorOrNil()
|
opStats.readErr = merr.ErrorOrNil()
|
||||||
@ -307,7 +320,9 @@ func selectorToReasons(sel selectors.Selector) []kopia.Reason {
|
|||||||
return reasons
|
return reasons
|
||||||
}
|
}
|
||||||
|
|
||||||
func builderFromReason(tenant string, r kopia.Reason) (*path.Builder, error) {
|
func builderFromReason(ctx context.Context, tenant string, r kopia.Reason) (*path.Builder, error) {
|
||||||
|
ctx = clues.Add(ctx, "category", r.Category.String())
|
||||||
|
|
||||||
// This is hacky, but we want the path package to format the path the right
|
// This is hacky, but we want the path package to format the path the right
|
||||||
// way (e.x. proper order for service, category, etc), but we don't care about
|
// way (e.x. proper order for service, category, etc), but we don't care about
|
||||||
// the folders after the prefix.
|
// the folders after the prefix.
|
||||||
@ -319,12 +334,7 @@ func builderFromReason(tenant string, r kopia.Reason) (*path.Builder, error) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(
|
return nil, clues.Wrap(err, "building path").WithMap(clues.Values(ctx))
|
||||||
err,
|
|
||||||
"building path for service %s category %s",
|
|
||||||
r.Service.String(),
|
|
||||||
r.Category.String(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.ToBuilder().Dir(), nil
|
return p.ToBuilder().Dir(), nil
|
||||||
@ -367,7 +377,7 @@ func consumeBackupDataCollections(
|
|||||||
categories := map[string]struct{}{}
|
categories := map[string]struct{}{}
|
||||||
|
|
||||||
for _, reason := range m.Reasons {
|
for _, reason := range m.Reasons {
|
||||||
pb, err := builderFromReason(tenantID, reason)
|
pb, err := builderFromReason(ctx, tenantID, reason)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, errors.Wrap(err, "getting subtree paths for bases")
|
return nil, nil, nil, errors.Wrap(err, "getting subtree paths for bases")
|
||||||
}
|
}
|
||||||
@ -461,6 +471,8 @@ func mergeDetails(
|
|||||||
var addedEntries int
|
var addedEntries int
|
||||||
|
|
||||||
for _, man := range mans {
|
for _, man := range mans {
|
||||||
|
mctx := clues.Add(ctx, "manifest_id", man.ID)
|
||||||
|
|
||||||
// For now skip snapshots that aren't complete. We will need to revisit this
|
// For now skip snapshots that aren't complete. We will need to revisit this
|
||||||
// when we tackle restartability.
|
// when we tackle restartability.
|
||||||
if len(man.IncompleteReason) > 0 {
|
if len(man.IncompleteReason) > 0 {
|
||||||
@ -469,9 +481,11 @@ func mergeDetails(
|
|||||||
|
|
||||||
bID, ok := man.GetTag(kopia.TagBackupID)
|
bID, ok := man.GetTag(kopia.TagBackupID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.Errorf("no backup ID in snapshot manifest with ID %s", man.ID)
|
return clues.New("no backup ID in snapshot manifest").WithMap(clues.Values(mctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mctx = clues.Add(mctx, "manifest_backup_id", bID)
|
||||||
|
|
||||||
_, baseDeets, err := getBackupAndDetailsFromID(
|
_, baseDeets, err := getBackupAndDetailsFromID(
|
||||||
ctx,
|
ctx,
|
||||||
model.StableID(bID),
|
model.StableID(bID),
|
||||||
@ -479,18 +493,15 @@ func mergeDetails(
|
|||||||
detailsStore,
|
detailsStore,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "backup fetching base details for backup %s", bID)
|
return clues.New("fetching base details for backup").WithMap(clues.Values(mctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range baseDeets.Items() {
|
for _, entry := range baseDeets.Items() {
|
||||||
rr, err := path.FromDataLayerPath(entry.RepoRef, true)
|
rr, err := path.FromDataLayerPath(entry.RepoRef, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(
|
return clues.New("parsing base item info path").
|
||||||
err,
|
WithMap(clues.Values(mctx)).
|
||||||
"parsing base item info path %s in backup %s",
|
With("repo_ref", entry.RepoRef) // todo: pii
|
||||||
entry.RepoRef,
|
|
||||||
bID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Although this base has an entry it may not be the most recent. Check
|
// Although this base has an entry it may not be the most recent. Check
|
||||||
@ -513,11 +524,7 @@ func mergeDetails(
|
|||||||
// Fixup paths in the item.
|
// Fixup paths in the item.
|
||||||
item := entry.ItemInfo
|
item := entry.ItemInfo
|
||||||
if err := details.UpdateItem(&item, newPath); err != nil {
|
if err := details.UpdateItem(&item, newPath); err != nil {
|
||||||
return errors.Wrapf(
|
return clues.New("updating item details").WithMap(clues.Values(mctx))
|
||||||
err,
|
|
||||||
"updating item info for entry from backup %s",
|
|
||||||
bID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(ashmrtn): This may need updated if we start using this merge
|
// TODO(ashmrtn): This may need updated if we start using this merge
|
||||||
@ -542,11 +549,9 @@ func mergeDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if addedEntries != len(shortRefsFromPrevBackup) {
|
if addedEntries != len(shortRefsFromPrevBackup) {
|
||||||
return errors.Errorf(
|
return clues.New("incomplete migration of backup details").
|
||||||
"incomplete migration of backup details: found %v of %v expected items",
|
WithMap(clues.Values(ctx)).
|
||||||
addedEntries,
|
WithAll("item_count", addedEntries, "expected_item_count", len(shortRefsFromPrevBackup))
|
||||||
len(shortRefsFromPrevBackup),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -568,6 +573,7 @@ func (op *BackupOperation) persistResults(
|
|||||||
if opStats.readErr != nil || opStats.writeErr != nil {
|
if opStats.readErr != nil || opStats.writeErr != nil {
|
||||||
op.Status = Failed
|
op.Status = Failed
|
||||||
|
|
||||||
|
// TODO(keepers): replace with fault.Errors handling.
|
||||||
return multierror.Append(
|
return multierror.Append(
|
||||||
errors.New("errors prevented the operation from processing"),
|
errors.New("errors prevented the operation from processing"),
|
||||||
opStats.readErr,
|
opStats.readErr,
|
||||||
@ -594,15 +600,18 @@ func (op *BackupOperation) createBackupModels(
|
|||||||
snapID string,
|
snapID string,
|
||||||
backupDetails *details.Details,
|
backupDetails *details.Details,
|
||||||
) error {
|
) error {
|
||||||
|
ctx = clues.Add(ctx, "snapshot_id", snapID)
|
||||||
|
|
||||||
if backupDetails == nil {
|
if backupDetails == nil {
|
||||||
return errors.New("no backup details to record")
|
return clues.New("no backup details to record").WithMap(clues.Values(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
detailsID, err := detailsStore.WriteBackupDetails(ctx, backupDetails)
|
detailsID, err := detailsStore.WriteBackupDetails(ctx, backupDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "creating backupdetails model")
|
return clues.Wrap(err, "creating backupDetails model").WithMap(clues.Values(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx = clues.Add(ctx, "details_id", detailsID)
|
||||||
b := backup.New(
|
b := backup.New(
|
||||||
snapID, detailsID, op.Status.String(),
|
snapID, detailsID, op.Status.String(),
|
||||||
op.Results.BackupID,
|
op.Results.BackupID,
|
||||||
@ -612,9 +621,8 @@ func (op *BackupOperation) createBackupModels(
|
|||||||
op.Errors,
|
op.Errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = op.store.Put(ctx, model.BackupSchema, b)
|
if err = op.store.Put(ctx, model.BackupSchema, b); err != nil {
|
||||||
if err != nil {
|
return clues.Wrap(err, "creating backup model").WithMap(clues.Values(ctx))
|
||||||
return errors.Wrap(err, "creating backup model")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dur := op.Results.CompletedAt.Sub(op.Results.StartedAt)
|
dur := op.Results.CompletedAt.Sub(op.Results.StartedAt)
|
||||||
|
|||||||
@ -432,258 +432,6 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BackupOpSuite) TestBackupOperation_VerifyDistinctBases() {
|
|
||||||
const user = "a-user"
|
|
||||||
|
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
input []*kopia.ManifestEntry
|
|
||||||
errCheck assert.ErrorAssertionFunc
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "SingleManifestMultipleReasons",
|
|
||||||
input: []*kopia.ManifestEntry{
|
|
||||||
{
|
|
||||||
Manifest: &snapshot.Manifest{
|
|
||||||
ID: "id1",
|
|
||||||
},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EventsCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errCheck: assert.NoError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "MultipleManifestsDistinctReason",
|
|
||||||
input: []*kopia.ManifestEntry{
|
|
||||||
{
|
|
||||||
Manifest: &snapshot.Manifest{
|
|
||||||
ID: "id1",
|
|
||||||
},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Manifest: &snapshot.Manifest{
|
|
||||||
ID: "id2",
|
|
||||||
},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EventsCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errCheck: assert.NoError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "MultipleManifestsSameReason",
|
|
||||||
input: []*kopia.ManifestEntry{
|
|
||||||
{
|
|
||||||
Manifest: &snapshot.Manifest{
|
|
||||||
ID: "id1",
|
|
||||||
},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Manifest: &snapshot.Manifest{
|
|
||||||
ID: "id2",
|
|
||||||
},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errCheck: assert.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "MultipleManifestsSameReasonOneIncomplete",
|
|
||||||
input: []*kopia.ManifestEntry{
|
|
||||||
{
|
|
||||||
Manifest: &snapshot.Manifest{
|
|
||||||
ID: "id1",
|
|
||||||
},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Manifest: &snapshot.Manifest{
|
|
||||||
ID: "id2",
|
|
||||||
IncompleteReason: "checkpoint",
|
|
||||||
},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: user,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errCheck: assert.NoError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range table {
|
|
||||||
suite.T().Run(test.name, func(t *testing.T) {
|
|
||||||
test.errCheck(t, verifyDistinctBases(test.input))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *BackupOpSuite) TestBackupOperation_CollectMetadata() {
|
|
||||||
var (
|
|
||||||
tenant = "a-tenant"
|
|
||||||
resourceOwner = "a-user"
|
|
||||||
fileNames = []string{
|
|
||||||
"delta",
|
|
||||||
"paths",
|
|
||||||
}
|
|
||||||
|
|
||||||
emailDeltaPath = makeMetadataPath(
|
|
||||||
suite.T(),
|
|
||||||
tenant,
|
|
||||||
path.ExchangeService,
|
|
||||||
resourceOwner,
|
|
||||||
path.EmailCategory,
|
|
||||||
fileNames[0],
|
|
||||||
)
|
|
||||||
emailPathsPath = makeMetadataPath(
|
|
||||||
suite.T(),
|
|
||||||
tenant,
|
|
||||||
path.ExchangeService,
|
|
||||||
resourceOwner,
|
|
||||||
path.EmailCategory,
|
|
||||||
fileNames[1],
|
|
||||||
)
|
|
||||||
contactsDeltaPath = makeMetadataPath(
|
|
||||||
suite.T(),
|
|
||||||
tenant,
|
|
||||||
path.ExchangeService,
|
|
||||||
resourceOwner,
|
|
||||||
path.ContactsCategory,
|
|
||||||
fileNames[0],
|
|
||||||
)
|
|
||||||
contactsPathsPath = makeMetadataPath(
|
|
||||||
suite.T(),
|
|
||||||
tenant,
|
|
||||||
path.ExchangeService,
|
|
||||||
resourceOwner,
|
|
||||||
path.ContactsCategory,
|
|
||||||
fileNames[1],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
inputMan *kopia.ManifestEntry
|
|
||||||
inputFiles []string
|
|
||||||
expected []path.Path
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "SingleReasonSingleFile",
|
|
||||||
inputMan: &kopia.ManifestEntry{
|
|
||||||
Manifest: &snapshot.Manifest{},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: resourceOwner,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
inputFiles: []string{fileNames[0]},
|
|
||||||
expected: []path.Path{emailDeltaPath},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SingleReasonMultipleFiles",
|
|
||||||
inputMan: &kopia.ManifestEntry{
|
|
||||||
Manifest: &snapshot.Manifest{},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: resourceOwner,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
inputFiles: fileNames,
|
|
||||||
expected: []path.Path{emailDeltaPath, emailPathsPath},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "MultipleReasonsMultipleFiles",
|
|
||||||
inputMan: &kopia.ManifestEntry{
|
|
||||||
Manifest: &snapshot.Manifest{},
|
|
||||||
Reasons: []kopia.Reason{
|
|
||||||
{
|
|
||||||
ResourceOwner: resourceOwner,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.EmailCategory,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ResourceOwner: resourceOwner,
|
|
||||||
Service: path.ExchangeService,
|
|
||||||
Category: path.ContactsCategory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
inputFiles: fileNames,
|
|
||||||
expected: []path.Path{
|
|
||||||
emailDeltaPath,
|
|
||||||
emailPathsPath,
|
|
||||||
contactsDeltaPath,
|
|
||||||
contactsPathsPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range table {
|
|
||||||
suite.T().Run(test.name, func(t *testing.T) {
|
|
||||||
ctx, flush := tester.NewContext()
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
mr := &mockRestorer{}
|
|
||||||
|
|
||||||
_, err := collectMetadata(ctx, mr, test.inputMan, test.inputFiles, tenant)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
checkPaths(t, test.expected, mr.gotPaths)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *BackupOpSuite) TestBackupOperation_ConsumeBackupDataCollections_Paths() {
|
func (suite *BackupOpSuite) TestBackupOperation_ConsumeBackupDataCollections_Paths() {
|
||||||
var (
|
var (
|
||||||
tenant = "a-tenant"
|
tenant = "a-tenant"
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package operations
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
multierror "github.com/hashicorp/go-multierror"
|
"github.com/alcionai/clues"
|
||||||
"github.com/kopia/kopia/repo/manifest"
|
"github.com/kopia/kopia/repo/manifest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/kopia"
|
"github.com/alcionai/corso/src/internal/kopia"
|
||||||
"github.com/alcionai/corso/src/internal/model"
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
"github.com/alcionai/corso/src/pkg/backup"
|
"github.com/alcionai/corso/src/pkg/backup"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
@ -44,6 +45,7 @@ func produceManifestsAndMetadata(
|
|||||||
reasons []kopia.Reason,
|
reasons []kopia.Reason,
|
||||||
tenantID string,
|
tenantID string,
|
||||||
getMetadata bool,
|
getMetadata bool,
|
||||||
|
errs fault.Adder,
|
||||||
) ([]*kopia.ManifestEntry, []data.Collection, bool, error) {
|
) ([]*kopia.ManifestEntry, []data.Collection, bool, error) {
|
||||||
var (
|
var (
|
||||||
metadataFiles = graph.AllMetadataFileNames()
|
metadataFiles = graph.AllMetadataFileNames()
|
||||||
@ -68,12 +70,10 @@ func produceManifestsAndMetadata(
|
|||||||
//
|
//
|
||||||
// TODO(ashmrtn): This may need updating if we start sourcing item backup
|
// TODO(ashmrtn): This may need updating if we start sourcing item backup
|
||||||
// details from previous snapshots when using kopia-assisted incrementals.
|
// details from previous snapshots when using kopia-assisted incrementals.
|
||||||
if err := verifyDistinctBases(ms); err != nil {
|
if err := verifyDistinctBases(ctx, ms, errs); err != nil {
|
||||||
logger.Ctx(ctx).Warnw(
|
logger.Ctx(ctx).With("error", err).Infow(
|
||||||
"base snapshot collision, falling back to full backup",
|
"base snapshot collision, falling back to full backup",
|
||||||
"error",
|
clues.Slice(ctx)...)
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ms, nil, false, nil
|
return ms, nil, false, nil
|
||||||
}
|
}
|
||||||
@ -83,40 +83,41 @@ func produceManifestsAndMetadata(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mctx := clues.Add(ctx, "manifest_id", man.ID)
|
||||||
|
|
||||||
bID, ok := man.GetTag(kopia.TagBackupID)
|
bID, ok := man.GetTag(kopia.TagBackupID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, false, errors.New("snapshot manifest missing backup ID")
|
err = clues.New("snapshot manifest missing backup ID").WithMap(clues.Values(mctx))
|
||||||
|
return nil, nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dID, _, err := gdi.GetDetailsIDFromBackupID(ctx, model.StableID(bID))
|
mctx = clues.Add(mctx, "manifest_backup_id", man.ID)
|
||||||
|
|
||||||
|
dID, _, err := gdi.GetDetailsIDFromBackupID(mctx, model.StableID(bID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if no backup exists for any of the complete manifests, we want
|
// if no backup exists for any of the complete manifests, we want
|
||||||
// to fall back to a complete backup.
|
// to fall back to a complete backup.
|
||||||
if errors.Is(err, kopia.ErrNotFound) {
|
if errors.Is(err, kopia.ErrNotFound) {
|
||||||
logger.Ctx(ctx).Infow(
|
logger.Ctx(ctx).Infow("backup missing, falling back to full backup", clues.Slice(mctx)...)
|
||||||
"backup missing, falling back to full backup",
|
|
||||||
"backup_id", bID)
|
|
||||||
|
|
||||||
return ms, nil, false, nil
|
return ms, nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, false, errors.Wrap(err, "retrieving prior backup data")
|
return nil, nil, false, errors.Wrap(err, "retrieving prior backup data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mctx = clues.Add(mctx, "manifest_details_id", dID)
|
||||||
|
|
||||||
// if no detailsID exists for any of the complete manifests, we want
|
// if no detailsID exists for any of the complete manifests, we want
|
||||||
// to fall back to a complete backup. This is a temporary prevention
|
// to fall back to a complete backup. This is a temporary prevention
|
||||||
// mechanism to keep backups from falling into a perpetually bad state.
|
// mechanism to keep backups from falling into a perpetually bad state.
|
||||||
// This makes an assumption that the ID points to a populated set of
|
// This makes an assumption that the ID points to a populated set of
|
||||||
// details; we aren't doing the work to look them up.
|
// details; we aren't doing the work to look them up.
|
||||||
if len(dID) == 0 {
|
if len(dID) == 0 {
|
||||||
logger.Ctx(ctx).Infow(
|
logger.Ctx(ctx).Infow("backup missing details ID, falling back to full backup", clues.Slice(mctx)...)
|
||||||
"backup missing details ID, falling back to full backup",
|
|
||||||
"backup_id", bID)
|
|
||||||
|
|
||||||
return ms, nil, false, nil
|
return ms, nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
colls, err := collectMetadata(ctx, mr, man, metadataFiles, tenantID)
|
colls, err := collectMetadata(mctx, mr, man, metadataFiles, tenantID)
|
||||||
if err != nil && !errors.Is(err, kopia.ErrNotFound) {
|
if err != nil && !errors.Is(err, kopia.ErrNotFound) {
|
||||||
// prior metadata isn't guaranteed to exist.
|
// prior metadata isn't guaranteed to exist.
|
||||||
// if it doesn't, we'll just have to do a
|
// if it doesn't, we'll just have to do a
|
||||||
@ -134,9 +135,9 @@ func produceManifestsAndMetadata(
|
|||||||
// of manifests, that each manifest's Reason (owner, service, category) is only
|
// of manifests, that each manifest's Reason (owner, service, category) is only
|
||||||
// included once. If a reason is duplicated by any two manifests, an error is
|
// included once. If a reason is duplicated by any two manifests, an error is
|
||||||
// returned.
|
// returned.
|
||||||
func verifyDistinctBases(mans []*kopia.ManifestEntry) error {
|
func verifyDistinctBases(ctx context.Context, mans []*kopia.ManifestEntry, errs fault.Adder) error {
|
||||||
var (
|
var (
|
||||||
errs *multierror.Error
|
failed bool
|
||||||
reasons = map[string]manifest.ID{}
|
reasons = map[string]manifest.ID{}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -155,10 +156,11 @@ func verifyDistinctBases(mans []*kopia.ManifestEntry) error {
|
|||||||
reasonKey := reason.ResourceOwner + reason.Service.String() + reason.Category.String()
|
reasonKey := reason.ResourceOwner + reason.Service.String() + reason.Category.String()
|
||||||
|
|
||||||
if b, ok := reasons[reasonKey]; ok {
|
if b, ok := reasons[reasonKey]; ok {
|
||||||
errs = multierror.Append(errs, errors.Errorf(
|
failed = true
|
||||||
"multiple base snapshots source data for %s %s. IDs: %s, %s",
|
|
||||||
reason.Service, reason.Category, b, man.ID,
|
errs.Add(clues.New("manifests have overlapping reasons").
|
||||||
))
|
WithMap(clues.Values(ctx)).
|
||||||
|
With("other_manifest_id", b))
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -167,7 +169,11 @@ func verifyDistinctBases(mans []*kopia.ManifestEntry) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errs.ErrorOrNil()
|
if failed {
|
||||||
|
return clues.New("multiple base snapshots qualify").WithMap(clues.Values(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectMetadata retrieves all metadata files associated with the manifest.
|
// collectMetadata retrieves all metadata files associated with the manifest.
|
||||||
@ -191,7 +197,9 @@ func collectMetadata(
|
|||||||
reason.Category,
|
reason.Category,
|
||||||
true)
|
true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "building metadata path")
|
return nil, clues.
|
||||||
|
Wrap(err, "building metadata path").
|
||||||
|
WithAll("metadata_file", fn, "category", reason.Category)
|
||||||
}
|
}
|
||||||
|
|
||||||
paths = append(paths, p)
|
paths = append(paths, p)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/model"
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/pkg/backup"
|
"github.com/alcionai/corso/src/pkg/backup"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault/mock"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -400,7 +401,10 @@ func (suite *OperationsManifestsUnitSuite) TestVerifyDistinctBases() {
|
|||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.T().Run(test.name, func(t *testing.T) {
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
err := verifyDistinctBases(test.mans)
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
err := verifyDistinctBases(ctx, test.mans, mock.NewAdder())
|
||||||
test.expect(t, err)
|
test.expect(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -646,6 +650,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
|
|||||||
ctx, flush := tester.NewContext()
|
ctx, flush := tester.NewContext()
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
|
ma := mock.NewAdder()
|
||||||
|
|
||||||
mans, dcs, b, err := produceManifestsAndMetadata(
|
mans, dcs, b, err := produceManifestsAndMetadata(
|
||||||
ctx,
|
ctx,
|
||||||
&test.mr,
|
&test.mr,
|
||||||
@ -653,7 +659,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
|
|||||||
test.reasons,
|
test.reasons,
|
||||||
tid,
|
tid,
|
||||||
test.getMeta,
|
test.getMeta,
|
||||||
)
|
ma)
|
||||||
test.assertErr(t, err)
|
test.assertErr(t, err)
|
||||||
test.assertB(t, b)
|
test.assertB(t, b)
|
||||||
|
|
||||||
@ -683,3 +689,270 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// older tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type BackupManifestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupManifestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(BackupOpSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupManifestSuite) TestBackupOperation_VerifyDistinctBases() {
|
||||||
|
const user = "a-user"
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
input []*kopia.ManifestEntry
|
||||||
|
errCheck assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SingleManifestMultipleReasons",
|
||||||
|
input: []*kopia.ManifestEntry{
|
||||||
|
{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: "id1",
|
||||||
|
},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EventsCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleManifestsDistinctReason",
|
||||||
|
input: []*kopia.ManifestEntry{
|
||||||
|
{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: "id1",
|
||||||
|
},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: "id2",
|
||||||
|
},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EventsCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleManifestsSameReason",
|
||||||
|
input: []*kopia.ManifestEntry{
|
||||||
|
{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: "id1",
|
||||||
|
},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: "id2",
|
||||||
|
},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errCheck: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleManifestsSameReasonOneIncomplete",
|
||||||
|
input: []*kopia.ManifestEntry{
|
||||||
|
{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: "id1",
|
||||||
|
},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: "id2",
|
||||||
|
IncompleteReason: "checkpoint",
|
||||||
|
},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: user,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
test.errCheck(t, verifyDistinctBases(ctx, test.input, mock.NewAdder()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupManifestSuite) TestBackupOperation_CollectMetadata() {
|
||||||
|
var (
|
||||||
|
tenant = "a-tenant"
|
||||||
|
resourceOwner = "a-user"
|
||||||
|
fileNames = []string{
|
||||||
|
"delta",
|
||||||
|
"paths",
|
||||||
|
}
|
||||||
|
|
||||||
|
emailDeltaPath = makeMetadataPath(
|
||||||
|
suite.T(),
|
||||||
|
tenant,
|
||||||
|
path.ExchangeService,
|
||||||
|
resourceOwner,
|
||||||
|
path.EmailCategory,
|
||||||
|
fileNames[0],
|
||||||
|
)
|
||||||
|
emailPathsPath = makeMetadataPath(
|
||||||
|
suite.T(),
|
||||||
|
tenant,
|
||||||
|
path.ExchangeService,
|
||||||
|
resourceOwner,
|
||||||
|
path.EmailCategory,
|
||||||
|
fileNames[1],
|
||||||
|
)
|
||||||
|
contactsDeltaPath = makeMetadataPath(
|
||||||
|
suite.T(),
|
||||||
|
tenant,
|
||||||
|
path.ExchangeService,
|
||||||
|
resourceOwner,
|
||||||
|
path.ContactsCategory,
|
||||||
|
fileNames[0],
|
||||||
|
)
|
||||||
|
contactsPathsPath = makeMetadataPath(
|
||||||
|
suite.T(),
|
||||||
|
tenant,
|
||||||
|
path.ExchangeService,
|
||||||
|
resourceOwner,
|
||||||
|
path.ContactsCategory,
|
||||||
|
fileNames[1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
inputMan *kopia.ManifestEntry
|
||||||
|
inputFiles []string
|
||||||
|
expected []path.Path
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SingleReasonSingleFile",
|
||||||
|
inputMan: &kopia.ManifestEntry{
|
||||||
|
Manifest: &snapshot.Manifest{},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputFiles: []string{fileNames[0]},
|
||||||
|
expected: []path.Path{emailDeltaPath},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleReasonMultipleFiles",
|
||||||
|
inputMan: &kopia.ManifestEntry{
|
||||||
|
Manifest: &snapshot.Manifest{},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputFiles: fileNames,
|
||||||
|
expected: []path.Path{emailDeltaPath, emailPathsPath},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleReasonsMultipleFiles",
|
||||||
|
inputMan: &kopia.ManifestEntry{
|
||||||
|
Manifest: &snapshot.Manifest{},
|
||||||
|
Reasons: []kopia.Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.EmailCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
Service: path.ExchangeService,
|
||||||
|
Category: path.ContactsCategory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputFiles: fileNames,
|
||||||
|
expected: []path.Path{
|
||||||
|
emailDeltaPath,
|
||||||
|
emailPathsPath,
|
||||||
|
contactsDeltaPath,
|
||||||
|
contactsPathsPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
mr := &mockRestorer{}
|
||||||
|
|
||||||
|
_, err := collectMetadata(ctx, mr, test.inputMan, test.inputFiles, tenant)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
checkPaths(t, test.expected, mr.gotPaths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -96,7 +96,9 @@ func (e *Errors) setErr(err error) *Errors {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: introduce Adder interface
|
type Adder interface {
|
||||||
|
Add(err error) *Errors
|
||||||
|
}
|
||||||
|
|
||||||
// Add appends the error to the slice of recoverable and
|
// Add appends the error to the slice of recoverable and
|
||||||
// iterated errors (ie: errors.errs). If failFast is true,
|
// iterated errors (ie: errors.errs). If failFast is true,
|
||||||
|
|||||||
17
src/pkg/fault/mock/mock.go
Normal file
17
src/pkg/fault/mock/mock.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package mock
|
||||||
|
|
||||||
|
import "github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
|
||||||
|
// Adder mocks an adder interface for testing.
|
||||||
|
type Adder struct {
|
||||||
|
Errs []error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdder() *Adder {
|
||||||
|
return &Adder{Errs: []error{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ma *Adder) Add(err error) *fault.Errors {
|
||||||
|
ma.Errs = append(ma.Errs, err)
|
||||||
|
return fault.New(false)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user