From aca3d856af37def63353e8d24ceef754267f98f1 Mon Sep 17 00:00:00 2001 From: Keepers <104464746+ryanfkeepers@users.noreply.github.com> Date: Thu, 28 Jul 2022 13:25:16 -0600 Subject: [PATCH] add printable backup struct and tabular formatting (#435) --- src/go.mod | 1 - src/pkg/backup/backup.go | 96 ++++++++------- src/pkg/backup/backup_test.go | 163 ++++++------------------- src/pkg/backup/details/details_test.go | 105 +++++++++++++++- src/pkg/selectors/selectors.go | 9 +- 5 files changed, 195 insertions(+), 179 deletions(-) diff --git a/src/go.mod b/src/go.mod index 693d67fc9..73a18d0b0 100644 --- a/src/go.mod +++ b/src/go.mod @@ -19,7 +19,6 @@ require ( github.com/stretchr/testify v1.8.0 github.com/tidwall/pretty v1.2.0 github.com/tomlazar/table v0.1.2 - github.com/zeebo/assert v1.1.0 go.uber.org/zap v1.21.0 golang.org/x/tools v0.1.11 ) diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index 0c6d559c0..4df66b17d 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -1,7 +1,7 @@ package backup import ( - "strconv" + "fmt" "time" "github.com/alcionai/corso/internal/common" @@ -34,50 +34,6 @@ type Backup struct { stats.StartAndEndTime } -// MinimumPrintable reduces the Backup to its minimally printable details. -func (b Backup) MinimumPrintable() any { - // todo: implement printable backup struct - return b -} - -// Headers returns the human-readable names of properties in a Backup -// for printing out to a terminal in a columnar display. -func (b Backup) Headers() []string { - return []string{ - "Creation Time", - "Stable ID", - "Snapshot ID", - "Details ID", - "Status", - "Selectors", - "Items Read", - "Items Written", - "Read Errors", - "Write Errors", - "Started At", - "Completed At", - } -} - -// Values returns the values matching the Headers list for printing -// out to a terminal in a columnar display. -func (b Backup) Values() []string { - return []string{ - common.FormatTime(b.CreationTime), - string(b.ID), - 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, status string, selector selectors.Selector, @@ -94,3 +50,53 @@ func New( StartAndEndTime: se, } } + +// -------------------------------------------------------------------------------- +// CLI Output +// -------------------------------------------------------------------------------- + +type Printable struct { + ID model.StableID `json:"id"` + ErrorCount int `json:"errorCount"` + StartedAt time.Time `json:"started at"` + Status string `json:"status"` + Version string `json:"version"` + Selectors selectors.Printable `json:"selectors"` +} + +// MinimumPrintable reduces the Backup to its minimally printable details. +func (b Backup) MinimumPrintable() any { + // todo: implement printable backup struct + return Printable{ + ID: b.ID, + ErrorCount: support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors), + StartedAt: b.StartedAt, + Status: b.Status, + Version: "0", + Selectors: b.Selectors.Printable(), + } +} + +// Headers returns the human-readable names of properties in a Backup +// for printing out to a terminal in a columnar display. +func (b Backup) Headers() []string { + return []string{ + "Started At", + "ID", + "Status", + "Selectors", + } +} + +// Values returns the values matching the Headers list for printing +// out to a terminal in a columnar display. +func (b Backup) Values() []string { + errCount := support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors) + status := fmt.Sprintf("%s (%d errors)", b.Status, errCount) + return []string{ + common.FormatTime(b.StartedAt), + string(b.ID), + status, + b.Selectors.Printable().Resources(), + } +} diff --git a/src/pkg/backup/backup_test.go b/src/pkg/backup/backup_test.go index 0b09fb394..3d34fec50 100644 --- a/src/pkg/backup/backup_test.go +++ b/src/pkg/backup/backup_test.go @@ -5,14 +5,14 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "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" ) @@ -24,19 +24,18 @@ func TestBackupSuite(t *testing.T) { suite.Run(t, new(BackupSuite)) } -func (suite *BackupSuite) TestBackup_HeadersValues() { - t := suite.T() - now := time.Now() - - b := backup.Backup{ +func stubBackup(t time.Time) backup.Backup { + sel := selectors.NewExchangeBackup() + sel.Include(sel.Users(selectors.Any())) + return backup.Backup{ BaseModel: model.BaseModel{ - ID: model.StableID("stable"), + ID: model.StableID("id"), }, - CreationTime: now, + CreationTime: t, SnapshotID: "snapshot", DetailsID: "details", Status: "status", - Selectors: selectors.Selector{}, + Selectors: sel.Selector, ReadWrites: stats.ReadWrites{ ItemsRead: 1, ItemsWritten: 1, @@ -44,138 +43,52 @@ func (suite *BackupSuite) TestBackup_HeadersValues() { WriteErrors: errors.New("1"), }, StartAndEndTime: stats.StartAndEndTime{ - StartedAt: now, - CompletedAt: now, + StartedAt: t, + CompletedAt: t, }, } +} + +func (suite *BackupSuite) TestBackup_HeadersValues() { + t := suite.T() + now := time.Now() + b := stubBackup(now) expectHs := []string{ - "Creation Time", - "Stable ID", - "Snapshot ID", - "Details ID", + "Started At", + "ID", "Status", "Selectors", - "Items Read", - "Items Written", - "Read Errors", - "Write Errors", - "Started At", - "Completed At", } hs := b.Headers() - assert.DeepEqual(t, expectHs, hs) + assert.Equal(t, expectHs, hs) nowFmt := common.FormatTime(now) expectVs := []string{ nowFmt, - "stable", - "snapshot", - "details", - "status", - "{}", - "1", - "1", - "1", - "1", - nowFmt, - nowFmt, + "id", + "status (2 errors)", + selectors.All, } vs := b.Values() - assert.DeepEqual(t, expectVs, vs) + assert.Equal(t, expectVs, vs) } -func (suite *BackupSuite) TestDetailsEntry_HeadersValues() { +func (suite *BackupSuite) TestBackup_MinimumPrintable() { + t := suite.T() now := time.Now() - nowStr := now.Format(time.RFC3339Nano) + b := stubBackup(now) - table := []struct { - name string - entry details.DetailsEntry - expectHs []string - expectVs []string - }{ - { - name: "no info", - entry: details.DetailsEntry{ - RepoRef: "reporef", - }, - expectHs: []string{"Repo Ref"}, - expectVs: []string{"reporef"}, - }, - { - name: "exhange info", - entry: details.DetailsEntry{ - RepoRef: "reporef", - ItemInfo: details.ItemInfo{ - Exchange: &details.ExchangeInfo{ - Sender: "sender", - Subject: "subject", - Received: now, - }, - }, - }, - expectHs: []string{"Repo Ref", "Sender", "Subject", "Received"}, - expectVs: []string{"reporef", "sender", "subject", nowStr}, - }, - { - name: "sharepoint info", - entry: details.DetailsEntry{ - RepoRef: "reporef", - ItemInfo: details.ItemInfo{ - Sharepoint: &details.SharepointInfo{}, - }, - }, - expectHs: []string{"Repo Ref"}, - expectVs: []string{"reporef"}, - }, - } + resultIface := b.MinimumPrintable() + result, ok := resultIface.(backup.Printable) + require.True(t, ok) - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - hs := test.entry.Headers() - assert.DeepEqual(t, test.expectHs, hs) - vs := test.entry.Values() - assert.DeepEqual(t, test.expectVs, vs) - }) - } -} - -func (suite *BackupSuite) TestDetailsModel_Path() { - table := []struct { - name string - ents []details.DetailsEntry - expect []string - }{ - { - name: "nil entries", - ents: nil, - expect: []string{}, - }, - { - name: "single entry", - ents: []details.DetailsEntry{ - {RepoRef: "abcde"}, - }, - expect: []string{"abcde"}, - }, - { - name: "multiple entries", - ents: []details.DetailsEntry{ - {RepoRef: "abcde"}, - {RepoRef: "12345"}, - }, - expect: []string{"abcde", "12345"}, - }, - } - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - d := details.Details{ - DetailsModel: details.DetailsModel{ - Entries: test.ents, - }, - } - assert.DeepEqual(t, test.expect, d.Paths()) - }) - } + assert.Equal(t, b.ID, result.ID, "id") + assert.Equal(t, 2, result.ErrorCount, "error count") + assert.Equal(t, now, result.StartedAt, "started at") + assert.Equal(t, b.Status, result.Status, "status") + + bselp := b.Selectors.Printable() + assert.Equal(t, bselp, result.Selectors, "selectors") + assert.Equal(t, bselp.Resources(), result.Selectors.Resources(), "selector resources") } diff --git a/src/pkg/backup/details/details_test.go b/src/pkg/backup/details/details_test.go index 50757487d..49ccf6e34 100644 --- a/src/pkg/backup/details/details_test.go +++ b/src/pkg/backup/details/details_test.go @@ -42,15 +42,15 @@ var ( } ) -type StoreDetailsUnitSuite struct { +type DetailsUnitSuite struct { suite.Suite } -func TestStoreDetailsUnitSuite(t *testing.T) { - suite.Run(t, new(StoreDetailsUnitSuite)) +func TestDetailsUnitSuite(t *testing.T) { + suite.Run(t, new(DetailsUnitSuite)) } -func (suite *StoreDetailsUnitSuite) TestGetDetails() { +func (suite *DetailsUnitSuite) TestGetDetails() { ctx := context.Background() table := []struct { @@ -82,7 +82,7 @@ func (suite *StoreDetailsUnitSuite) TestGetDetails() { } } -func (suite *StoreDetailsUnitSuite) TestGetDetailsFromBackupID() { +func (suite *DetailsUnitSuite) TestGetDetailsFromBackupID() { ctx := context.Background() table := []struct { @@ -114,3 +114,98 @@ func (suite *StoreDetailsUnitSuite) TestGetDetailsFromBackupID() { }) } } + +func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { + now := time.Now() + nowStr := now.Format(time.RFC3339Nano) + + table := []struct { + name string + entry details.DetailsEntry + expectHs []string + expectVs []string + }{ + { + name: "no info", + entry: details.DetailsEntry{ + RepoRef: "reporef", + }, + expectHs: []string{"Repo Ref"}, + expectVs: []string{"reporef"}, + }, + { + name: "exhange info", + entry: details.DetailsEntry{ + RepoRef: "reporef", + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + Sender: "sender", + Subject: "subject", + Received: now, + }, + }, + }, + expectHs: []string{"Repo Ref", "Sender", "Subject", "Received"}, + expectVs: []string{"reporef", "sender", "subject", nowStr}, + }, + { + name: "sharepoint info", + entry: details.DetailsEntry{ + RepoRef: "reporef", + ItemInfo: details.ItemInfo{ + Sharepoint: &details.SharepointInfo{}, + }, + }, + expectHs: []string{"Repo Ref"}, + expectVs: []string{"reporef"}, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + hs := test.entry.Headers() + assert.Equal(t, test.expectHs, hs) + vs := test.entry.Values() + assert.Equal(t, test.expectVs, vs) + }) + } +} + +func (suite *DetailsUnitSuite) TestDetailsModel_Path() { + table := []struct { + name string + ents []details.DetailsEntry + expect []string + }{ + { + name: "nil entries", + ents: nil, + expect: []string{}, + }, + { + name: "single entry", + ents: []details.DetailsEntry{ + {RepoRef: "abcde"}, + }, + expect: []string{"abcde"}, + }, + { + name: "multiple entries", + ents: []details.DetailsEntry{ + {RepoRef: "abcde"}, + {RepoRef: "12345"}, + }, + expect: []string{"abcde", "12345"}, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + d := details.Details{ + DetailsModel: details.DetailsModel{ + Entries: test.ents, + }, + } + assert.Equal(t, test.expect, d.Paths()) + }) + } +} diff --git a/src/pkg/selectors/selectors.go b/src/pkg/selectors/selectors.go index eed621963..45206c610 100644 --- a/src/pkg/selectors/selectors.go +++ b/src/pkg/selectors/selectors.go @@ -48,11 +48,14 @@ const ( // None() to any selector will force all matches() checks on that // selector to fail. // Ex: {user: u1, events: NoneTgt} => matches nothing. - NoneTgt = "" - + NoneTgt = "" delimiter = "," ) +// All is the resource name that gets output when the resource is AnyTgt. +// It is not used aside from printing resources. +const All = "All" + // --------------------------------------------------------------------------- // Selector // --------------------------------------------------------------------------- @@ -217,7 +220,7 @@ func toResourceTypeMap(ms []map[string]string) map[string][]string { for _, m := range ms { res := m[scopeKeyResource] if res == AnyTgt { - res = "All" + res = All } r[res] = addToSet(r[res], m[scopeKeyDataType]) }