From 343d581ea4786fa001e20164fe402fb0bc61e331 Mon Sep 17 00:00:00 2001 From: Adele Reed Date: Wed, 2 Oct 2024 19:01:18 -0700 Subject: [PATCH] What if I completely redid everything for fun --- cmd/copyEnumeratorInit.go | 49 +++++-------- cmd/removeEnumerator.go | 57 +++++++++++++-- cmd/sync.go | 12 +++- cmd/syncProcessor.go | 51 +++++--------- cmd/zc_processor.go | 25 ++++++- cmd/zt_remove_blob_test.go | 9 +-- common/fe-ste-models.go | 8 +++ e2etest/newe2e_resource_manager_getter.go | 9 ++- e2etest/newe2e_runazcopy_stdout.go | 41 ++--------- e2etest/newe2e_task_runazcopy.go | 5 +- e2etest/newe2e_task_validation.go | 84 +++++++++++++++++------ 11 files changed, 210 insertions(+), 140 deletions(-) diff --git a/cmd/copyEnumeratorInit.go b/cmd/copyEnumeratorInit.go index 4e05212e1..415e027b1 100755 --- a/cmd/copyEnumeratorInit.go +++ b/cmd/copyEnumeratorInit.go @@ -279,39 +279,24 @@ func (cca *CookedCopyCmdArgs) initEnumerator(jobPartOrder common.CopyJobPartOrde if cca.dryrunMode && shouldSendToSte { glcm.Dryrun(func(format common.OutputFormat) string { - if format == common.EOutputFormat.Json() { - jsonOutput, err := json.Marshal(transfer) - common.PanicIfErr(err) - return string(jsonOutput) - } else { - if cca.FromTo.From() == common.ELocation.Local() { - // formatting from local source - dryrunValue := fmt.Sprintf("DRYRUN: copy %v", common.ToShortPath(cca.Source.Value)) - if runtime.GOOS == "windows" { - dryrunValue += strings.ReplaceAll(srcRelPath, "/", "\\") - } else { // linux and mac - dryrunValue += srcRelPath - } - dryrunValue += fmt.Sprintf(" to %v%v", strings.Trim(cca.Destination.Value, "/"), dstRelPath) - return dryrunValue - } else if cca.FromTo.To() == common.ELocation.Local() { - // formatting to local source - dryrunValue := fmt.Sprintf("DRYRUN: copy %v%v to %v", - strings.Trim(cca.Source.Value, "/"), srcRelPath, - common.ToShortPath(cca.Destination.Value)) - if runtime.GOOS == "windows" { - dryrunValue += strings.ReplaceAll(dstRelPath, "/", "\\") - } else { // linux and mac - dryrunValue += dstRelPath - } - return dryrunValue - } else { - return fmt.Sprintf("DRYRUN: copy %v%v to %v%v", - cca.Source.Value, - srcRelPath, - cca.Destination.Value, - dstRelPath) + src := common.GenerateFullPath(cca.Source.Value, srcRelPath) + dst := common.GenerateFullPath(cca.Destination.Value, dstRelPath) + + switch format { + case common.EOutputFormat.Json(): + tx := DryrunTransfer{ + EntityType: transfer.EntityType, + BlobType: common.FromBlobType(transfer.BlobType), + FromTo: cca.FromTo, + Source: src, + Destination: dst, } + + buf, _ := json.Marshal(tx) + return string(buf) + default: + return fmt.Sprintf("DRYRUN: copy %v to %v", + src, dst) } }) return nil diff --git a/cmd/removeEnumerator.go b/cmd/removeEnumerator.go index 7ca0f70c3..e7ce7213a 100755 --- a/cmd/removeEnumerator.go +++ b/cmd/removeEnumerator.go @@ -22,6 +22,7 @@ package cmd import ( "context" + "encoding/json" "errors" "fmt" "github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake" @@ -229,8 +230,22 @@ func removeBfsResources(cca *CookedCopyCmdArgs) (err error) { func dryrunRemoveSingleDFSResource(ctx context.Context, dsc *service.Client, datalakeURLParts azdatalake.URLParts, recursive bool) error { //deleting a filesystem if datalakeURLParts.PathName == "" { - glcm.Dryrun(func(_ common.OutputFormat) string { - return fmt.Sprintf("DRYRUN: remove filesystem %s", datalakeURLParts.FileSystemName) + glcm.Dryrun(func(of common.OutputFormat) string { + switch of { + case of.Text(): + return fmt.Sprintf("DRYRUN: remove %s", dsc.NewFileSystemClient(datalakeURLParts.FileSystemName).DFSURL()) + case of.Json(): + tx := DryrunTransfer{ + EntityType: common.EEntityType.Folder(), + FromTo: common.EFromTo.BlobFSTrash(), + Source: dsc.NewFileSystemClient(datalakeURLParts.FileSystemName).DFSURL(), + } + + buf, _ := json.Marshal(tx) + return string(buf) + default: + panic("unsupported output format " + of.String()) + } }) return nil } @@ -246,8 +261,22 @@ func dryrunRemoveSingleDFSResource(ctx context.Context, dsc *service.Client, dat // then we should short-circuit and simply remove that file resourceType := common.IffNotNil(props.ResourceType, "") if strings.EqualFold(resourceType, "file") { - glcm.Dryrun(func(_ common.OutputFormat) string { - return fmt.Sprintf("DRYRUN: remove file %s", datalakeURLParts.PathName) + glcm.Dryrun(func(of common.OutputFormat) string { + switch of { + case of.Text(): + return fmt.Sprintf("DRYRUN: remove %s", directoryClient.DFSURL()) + case of.Json(): + tx := DryrunTransfer{ + EntityType: common.EEntityType.File(), + FromTo: common.EFromTo.BlobFSTrash(), + Source: directoryClient.DFSURL(), + } + + buf, _ := json.Marshal(tx) + return string(buf) + default: + panic("unsupported output format " + of.String()) + } }) return nil } @@ -267,8 +296,24 @@ func dryrunRemoveSingleDFSResource(ctx context.Context, dsc *service.Client, dat entityType = "file" } - glcm.Dryrun(func(_ common.OutputFormat) string { - return fmt.Sprintf("DRYRUN: remove %s %s", entityType, *v.Name) + glcm.Dryrun(func(of common.OutputFormat) string { + uri := dsc.NewFileSystemClient(datalakeURLParts.FileSystemName).NewFileClient(*v.Name).DFSURL() + + switch of { + case of.Text(): + return fmt.Sprintf("DRYRUN: remove %s", uri) + case of.Json(): + tx := DryrunTransfer{ + EntityType: common.Iff(entityType == "directory", common.EEntityType.Folder(), common.EEntityType.File()), + FromTo: common.EFromTo.BlobFSTrash(), + Source: uri, + } + + buf, _ := json.Marshal(tx) + return string(buf) + default: + panic("unsupported output format " + of.String()) + } }) } } diff --git a/cmd/sync.go b/cmd/sync.go index 1ab15a5c7..a369ead22 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -163,7 +163,17 @@ func (raw *rawSyncCmdArgs) cook() (cookedSyncCmdArgs, error) { jobsAdmin.JobsAdmin.LogToJobLog(LocalToFileShareWarnMsg, common.LogWarning) } if raw.dryrun { - glcm.Dryrun(func(_ common.OutputFormat) string { + glcm.Dryrun(func(of common.OutputFormat) string { + if of == common.EOutputFormat.Json() { + var out struct { + Warn string `json:"warn"` + } + + out.Warn = LocalToFileShareWarnMsg + buf, _ := json.Marshal(out) + return string(buf) + } + return fmt.Sprintf("DRYRUN: warn %s", LocalToFileShareWarnMsg) }) } diff --git a/cmd/syncProcessor.go b/cmd/syncProcessor.go index 2603157ec..96bed1572 100644 --- a/cmd/syncProcessor.go +++ b/cmd/syncProcessor.go @@ -27,7 +27,6 @@ import ( "net/url" "os" "path" - "runtime" "strings" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob" @@ -75,24 +74,6 @@ type interactiveDeleteProcessor struct { dryrunMode bool } -func newDeleteTransfer(object StoredObject) (newDeleteTransfer common.CopyTransfer) { - return common.CopyTransfer{ - Source: object.relativePath, - EntityType: object.entityType, - LastModifiedTime: object.lastModifiedTime, - SourceSize: object.size, - ContentType: object.contentType, - ContentEncoding: object.contentEncoding, - ContentDisposition: object.contentDisposition, - ContentLanguage: object.contentLanguage, - CacheControl: object.cacheControl, - Metadata: object.Metadata, - BlobType: object.blobType, - BlobVersionID: object.blobVersionID, - BlobTags: object.blobTags, - } -} - func (d *interactiveDeleteProcessor) removeImmediately(object StoredObject) (err error) { if d.shouldPromptUser { d.shouldDelete, d.shouldPromptUser = d.promptForConfirmation(object) // note down the user's decision @@ -105,22 +86,24 @@ func (d *interactiveDeleteProcessor) removeImmediately(object StoredObject) (err if d.dryrunMode { glcm.Dryrun(func(format common.OutputFormat) string { if format == common.EOutputFormat.Json() { - jsonOutput, err := json.Marshal(newDeleteTransfer(object)) + deleteTarget := common.ELocation.Local() + if d.objectTypeToDisplay != LocalFileObjectType { + _ = deleteTarget.Parse(d.objectTypeToDisplay) + } + + tx := DryrunTransfer{ + Source: common.GenerateFullPath(d.objectLocationToDisplay, object.relativePath), + BlobType: common.FromBlobType(object.blobType), + EntityType: object.entityType, + FromTo: common.FromToValue(deleteTarget, common.ELocation.Unknown()), + } + + jsonOutput, err := json.Marshal(tx) common.PanicIfErr(err) return string(jsonOutput) } else { // remove for sync - if d.objectTypeToDisplay == "local file" { // removing from local src - dryrunValue := fmt.Sprintf("DRYRUN: remove %v", common.ToShortPath(d.objectLocationToDisplay)) - if runtime.GOOS == "windows" { - dryrunValue += "\\" + strings.ReplaceAll(object.relativePath, "/", "\\") - } else { // linux and mac - dryrunValue += "/" + object.relativePath - } - return dryrunValue - } - return fmt.Sprintf("DRYRUN: remove %v/%v", - d.objectLocationToDisplay, - object.relativePath) + return fmt.Sprintf("DRYRUN: remove %v", + common.GenerateFullPath(d.objectLocationToDisplay, object.relativePath)) } }) return nil @@ -189,9 +172,11 @@ func newInteractiveDeleteProcessor(deleter objectProcessor, deleteDestination co } } +const LocalFileObjectType = "local file" + func newSyncLocalDeleteProcessor(cca *cookedSyncCmdArgs, fpo common.FolderPropertyOption) *interactiveDeleteProcessor { localDeleter := localFileDeleter{rootPath: cca.destination.ValueLocal(), fpo: fpo, folderManager: common.NewFolderDeletionManager(context.Background(), fpo, azcopyScanningLogger)} - return newInteractiveDeleteProcessor(localDeleter.deleteFile, cca.deleteDestination, "local file", cca.destination, cca.incrementDeletionCount, cca.dryrunMode) + return newInteractiveDeleteProcessor(localDeleter.deleteFile, cca.deleteDestination, LocalFileObjectType, cca.destination, cca.incrementDeletionCount, cca.dryrunMode) } type localFileDeleter struct { diff --git a/cmd/zc_processor.go b/cmd/zc_processor.go index 30d260cff..ca25e0055 100644 --- a/cmd/zc_processor.go +++ b/cmd/zc_processor.go @@ -64,6 +64,8 @@ func newCopyTransferProcessor(copyJobTemplate *common.CopyJobPartOrderRequest, n } type DryrunTransfer struct { + EntityType common.EntityType + BlobType common.BlobType FromTo common.FromTo Source string Destination string @@ -71,6 +73,8 @@ type DryrunTransfer struct { func (d *DryrunTransfer) UnmarshalJSON(bytes []byte) error { var surrogate struct { + EntityType string + BlobType string FromTo string Source string Destination string @@ -86,6 +90,16 @@ func (d *DryrunTransfer) UnmarshalJSON(bytes []byte) error { return fmt.Errorf("failed to parse fromto: %w", err) } + err = d.EntityType.Parse(surrogate.EntityType) + if err != nil { + return fmt.Errorf("failed to parse entity type: %w", err) + } + + err = d.BlobType.Parse(surrogate.BlobType) + if err != nil { + return fmt.Errorf("failed to parse entity type: %w", err) + } + d.Source = surrogate.Source d.Destination = surrogate.Destination @@ -94,10 +108,14 @@ func (d *DryrunTransfer) UnmarshalJSON(bytes []byte) error { func (d DryrunTransfer) MarshalJSON() ([]byte, error) { surrogate := struct { + EntityType string + BlobType string FromTo string Source string Destination string }{ + d.EntityType.String(), + d.BlobType.String(), d.FromTo.String(), d.Source, d.Destination, @@ -161,8 +179,10 @@ func (s *copyTransferProcessor) scheduleCopyTransfer(storedObject StoredObject) if format == common.EOutputFormat.Json() { tx := DryrunTransfer{ - FromTo: s.copyJobTemplate.FromTo, - Source: common.GenerateFullPath(s.copyJobTemplate.SourceRoot.Value, prettySrcRelativePath), + BlobType: common.FromBlobType(storedObject.blobType), + EntityType: storedObject.entityType, + FromTo: s.copyJobTemplate.FromTo, + Source: common.GenerateFullPath(s.copyJobTemplate.SourceRoot.Value, prettySrcRelativePath), } if fromTo.To() != common.ELocation.None() && fromTo.To() != common.ELocation.Unknown() { @@ -173,7 +193,6 @@ func (s *copyTransferProcessor) scheduleCopyTransfer(storedObject StoredObject) common.PanicIfErr(err) return string(jsonOutput) } else { - // if remove then To() will equal to common.ELocation.Unknown() if s.copyJobTemplate.FromTo.To() == common.ELocation.Unknown() { // remove return fmt.Sprintf("DRYRUN: remove %v", diff --git a/cmd/zt_remove_blob_test.go b/cmd/zt_remove_blob_test.go index 555417357..34a6cd286 100644 --- a/cmd/zt_remove_blob_test.go +++ b/cmd/zt_remove_blob_test.go @@ -606,14 +606,15 @@ func TestDryrunRemoveBlobsUnderContainerJson(t *testing.T) { a.Zero(len(mockedRPC.transfers)) msg := <-mockedLcm.dryrunLog - deleteTransfer := common.CopyTransfer{} + deleteTransfer := DryrunTransfer{} errMarshal := json.Unmarshal([]byte(msg), &deleteTransfer) a.Nil(errMarshal) // comparing some values of deleteTransfer - a.Equal(deleteTransfer.Source, "/"+blobName[0]) - a.Equal(deleteTransfer.Destination, "/"+blobName[0]) + targetUri := cc.NewBlobClient(blobName[0]).URL() + a.Equal(targetUri, deleteTransfer.Source) + a.Equal("", deleteTransfer.Destination) a.Equal("File", deleteTransfer.EntityType.String()) - a.Equal("BlockBlob", string(deleteTransfer.BlobType)) + a.Equal("BlockBlob", deleteTransfer.BlobType.String()) }) } diff --git a/common/fe-ste-models.go b/common/fe-ste-models.go index 9915a170c..6eba969a1 100644 --- a/common/fe-ste-models.go +++ b/common/fe-ste-models.go @@ -1575,6 +1575,14 @@ func (e EntityType) String() string { return enum.StringInt(e, reflect.TypeOf(e)) } +func (e *EntityType) Parse(s string) error { + val, err := enum.ParseInt(reflect.TypeOf(e), s, true, true) + if err == nil { + *e = val.(EntityType) + } + return err +} + //////////////////////////////////////////////////////////////// var EFolderPropertiesOption = FolderPropertyOption(0) diff --git a/e2etest/newe2e_resource_manager_getter.go b/e2etest/newe2e_resource_manager_getter.go index 0d4fc399c..8d3a4ed85 100644 --- a/e2etest/newe2e_resource_manager_getter.go +++ b/e2etest/newe2e_resource_manager_getter.go @@ -24,8 +24,13 @@ func GetRootResource(a Asserter, location common.Location, varOpts ...GetResourc return NewLocalContainer(a) case common.ELocation.Blob(), common.ELocation.BlobFS(), common.ELocation.File(): - // acct handles the dryrun case for us - acct := GetAccount(a, DerefOrDefault(opts.PreferredAccount, PrimaryStandardAcct)) + // do we have a hns acct attached, if so, and we're requesting blobfs, let's use it + defaultacct := PrimaryStandardAcct + if _, ok := AccountRegistry[PrimaryHNSAcct]; ok { + defaultacct = PrimaryHNSAcct + } + + acct := GetAccount(a, DerefOrDefault(opts.PreferredAccount, defaultacct)) return acct.GetService(a, location) default: a.Error(fmt.Sprintf("TODO: Location %s is not yet supported", location)) diff --git a/e2etest/newe2e_runazcopy_stdout.go b/e2etest/newe2e_runazcopy_stdout.go index 3d15e39b8..5c1585141 100644 --- a/e2etest/newe2e_runazcopy_stdout.go +++ b/e2etest/newe2e_runazcopy_stdout.go @@ -154,7 +154,7 @@ type AzCopyParsedCopySyncRemoveStdout struct { JobPlanFolder string LogFolder string - + InitMsg common.InitMsgJsonTemplate FinalStatus common.ListJobSummaryResponse } @@ -182,51 +182,22 @@ type AzCopyParsedDryrunStdout struct { listenChan chan<- cmd.DryrunTransfer Transfers []cmd.DryrunTransfer + Raw map[string]bool JsonMode bool } func (d *AzCopyParsedDryrunStdout) Write(p []byte) (n int, err error) { lines := strings.Split(string(p), "\n") for _, str := range lines { - if !d.JsonMode { - // DRYRUN: [to] - if !strings.HasPrefix(str, "DRYRUN: ") { - continue - } - - str = strings.TrimPrefix(str, "DRYRUN: ") - resources := strings.Split(str, " ") - - var tx cmd.DryrunTransfer - - switch strings.ToLower(resources[0]) { - case "remove": - tx.FromTo = (common.FromTo(tx.FromTo.From()) << 8) | common.FromTo(common.ELocation.Unknown()) - case "set-properties": - tx.FromTo = (common.FromTo(tx.FromTo.From()) << 8) | common.FromTo(common.ELocation.None()) - case "copy": - tx.FromTo = d.fromTo - default: + if !d.JsonMode && strings.HasPrefix(str, "DRYRUN: ") { + if strings.HasPrefix(str, "DRYRUN: warn") { continue } - resources = resources[1:] - for _, v := range resources { - if strings.ToLower(v) == "to" { - continue - } - - if tx.Source == "" { - tx.Source = v - } else if tx.Destination == "" { - tx.Destination = v - } - } - - d.Transfers = append(d.Transfers, tx) + d.Raw[str] = true } else { var out common.JsonOutputTemplate - err = json.Unmarshal(p, &out) + err = json.Unmarshal([]byte(str), &out) if err != nil { continue } diff --git a/e2etest/newe2e_task_runazcopy.go b/e2etest/newe2e_task_runazcopy.go index afbb020df..669b03d41 100644 --- a/e2etest/newe2e_task_runazcopy.go +++ b/e2etest/newe2e_task_runazcopy.go @@ -255,7 +255,7 @@ func RunAzCopy(a ScenarioAsserter, commandSpec AzCopyCommand) (AzCopyStdout, *Az } out := []string{GlobalConfig.AzCopyExecutableConfig.ExecutablePath, string(commandSpec.Verb)} - + for _, v := range commandSpec.PositionalArgs { out = append(out, v) } @@ -293,7 +293,7 @@ func RunAzCopy(a ScenarioAsserter, commandSpec AzCopyCommand) (AzCopyStdout, *Az if out == nil { switch { case strings.EqualFold(flagMap["dry-run"], "true") && (strings.EqualFold(flagMap["output-type"], "json") || strings.EqualFold(flagMap["output-type"], "text") || flagMap["output-type"] == ""): // Dryrun has its own special sort of output, that supports non-json output. - jsonMode := strings.EqualFold(flagMap["outputType"], "json") + jsonMode := strings.EqualFold(flagMap["output-type"], "json") var fromTo common.FromTo if !jsonMode && len(commandSpec.Targets) >= 2 { fromTo = common.FromTo(commandSpec.Targets[0].Location())<<8 | common.FromTo(commandSpec.Targets[1].Location()) @@ -301,6 +301,7 @@ func RunAzCopy(a ScenarioAsserter, commandSpec AzCopyCommand) (AzCopyStdout, *Az out = &AzCopyParsedDryrunStdout{ JsonMode: jsonMode, fromTo: fromTo, + Raw: make(map[string]bool), } case !strings.EqualFold(flagMap["output-type"], "json"): // Won't parse non-computer-readable outputs out = &AzCopyRawStdout{} diff --git a/e2etest/newe2e_task_validation.go b/e2etest/newe2e_task_validation.go index d0896ecb2..9a18f5347 100644 --- a/e2etest/newe2e_task_validation.go +++ b/e2etest/newe2e_task_validation.go @@ -318,19 +318,21 @@ const ( DryrunOpProperties ) +var dryrunOpStr = map[DryrunOp]string{ + DryrunOpCopy: "copy", + DryrunOpDelete: "delete", + DryrunOpProperties: "set-properties", +} + // ValidateDryRunOutput validates output for items in the expected map; expected must equal output func ValidateDryRunOutput(a Asserter, output AzCopyStdout, rootSrc ResourceManager, rootDst ResourceManager, expected map[string]DryrunOp) { if dryrun, ok := a.(DryrunAsserter); ok && dryrun.Dryrun() { return } - a.AssertNow("Output must not be nil", Not{IsNil{}}, output) stdout, ok := output.(*AzCopyParsedDryrunStdout) a.AssertNow("Output must be dryrun stdout", Equal{}, ok, true) - // validation must have nothing in it, and nothing should miss in output. - validation := CloneMap(expected) - uriPrefs := GetURIOptions{ LocalOpts: LocalURIOpts{ PreferUNCPath: true, @@ -343,24 +345,62 @@ func ValidateDryRunOutput(a Asserter, output AzCopyStdout, rootSrc ResourceManag dstBase = rootDst.URI(uriPrefs) } - for _, v := range stdout.Transfers { - // Determine the op. - op := common.Iff(v.FromTo.IsDelete(), DryrunOpDelete, common.Iff(v.FromTo.IsSetProperties(), DryrunOpProperties, DryrunOpCopy)) - - // Try to find the item in expected. - relPath := strings.TrimPrefix( // Ensure we start with the rel path, not a separator - strings.ReplaceAll( // Isolate path separators - strings.TrimPrefix(v.Source, srcBase), // Isolate the relpath - "\\", "/", - ), - "/", - ) - //a.Log("base %s source %s rel %s", srcBase, v.Source, relPath) - expectedOp, ok := validation[relPath] - a.Assert(fmt.Sprintf("Expected %s in map", relPath), Equal{}, ok, true) - a.Assert(fmt.Sprintf("Expected %s to match", relPath), Equal{}, op, expectedOp) - if rootDst != nil { - a.Assert(fmt.Sprintf("Expected %s dest url to match expected dest url", relPath), Equal{}, v.Destination, common.GenerateFullPath(dstBase, relPath)) + if stdout.JsonMode { + // validation must have nothing in it, and nothing should miss in output. + validation := CloneMap(expected) + + for _, v := range stdout.Transfers { + // Determine the op. + op := common.Iff(v.FromTo.IsDelete(), DryrunOpDelete, common.Iff(v.FromTo.IsSetProperties(), DryrunOpProperties, DryrunOpCopy)) + + // Try to find the item in expected. + relPath := strings.TrimPrefix( // Ensure we start with the rel path, not a separator + strings.ReplaceAll( // Isolate path separators + strings.TrimPrefix(v.Source, srcBase), // Isolate the relpath + "\\", "/", + ), + "/", + ) + //a.Log("base %s source %s rel %s", srcBase, v.Source, relPath) + expectedOp, ok := validation[relPath] + a.Assert(fmt.Sprintf("Expected %s in map", relPath), Equal{}, ok, true) + a.Assert(fmt.Sprintf("Expected %s to match", relPath), Equal{}, op, expectedOp) + if rootDst != nil { + a.Assert(fmt.Sprintf("Expected %s dest url to match expected dest url", relPath), Equal{}, v.Destination, common.GenerateFullPath(dstBase, relPath)) + } + } + } else { + // It is useless to try to parse details from a user friendly statement. + // Instead, we should attempt to generate the user friendly statement, and validate that it existed from there. + validation := make(map[string]bool) + + for k, v := range expected { + from := common.GenerateFullPath(srcBase, k) + var to string + if rootDst != nil { + to = " to " + common.GenerateFullPath(dstBase, k) + } + + valStr := fmt.Sprintf("DRYRUN: %s %s%s", + dryrunOpStr[v], + from, + to, + ) + + validation[valStr] = true + } + + for k := range stdout.Raw { + _, ok := validation[k] + a.Assert(k+" wasn't present in validation", Equal{}, ok, true) + + if ok { + delete(validation, k) + } + } + + for k := range validation { + a.Assert(k+" wasn't present in output", Always{}) } } }