print stats after backup (#3128)

Prints backup stats to the CLI following completion.  In case of multiple users, the stats for each backup is printed at the end of the backup, rather than at the end of the command.

---

#### Does this PR need a docs update or release note?

- [x]  Yes, it's included

#### Type of change

- [x] 🌻 Feature

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
This commit is contained in:
Keepers 2023-04-20 18:17:40 -06:00 committed by GitHub
parent d5fac8a480
commit 6e982d6bdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 32 deletions

View File

@ -230,7 +230,13 @@ func runBackups(
} }
bIDs = append(bIDs, string(bo.Results.BackupID)) bIDs = append(bIDs, string(bo.Results.BackupID))
Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID)
if !DisplayJSONFormat() {
Infof(ctx, "Done\n")
printBackupStats(ctx, r, string(bo.Results.BackupID))
} else {
Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID)
}
} }
bups, berrs := r.Backups(ctx, bIDs) bups, berrs := r.Backups(ctx, bIDs)
@ -335,3 +341,13 @@ func getAccountAndConnect(ctx context.Context) (repository.Repository, *account.
func ifShow(flag string) bool { func ifShow(flag string) bool {
return strings.ToLower(strings.TrimSpace(flag)) == "show" return strings.ToLower(strings.TrimSpace(flag)) == "show"
} }
func printBackupStats(ctx context.Context, r repository.Repository, bid string) {
b, err := r.Backup(ctx, bid)
if err != nil {
logger.CtxErr(ctx, err).Error("finding backup immediately after backup operation completion")
}
b.ToPrintable().Stats.Print(ctx)
Info(ctx, " ")
}

View File

@ -50,8 +50,8 @@ func AddOutputFlag(cmd *cobra.Command) {
cobra.CheckErr(fs.MarkHidden("json-debug")) cobra.CheckErr(fs.MarkHidden("json-debug"))
} }
// JSONFormat returns true if the printer plans to output as json. // DisplayJSONFormat returns true if the printer plans to output as json.
func JSONFormat() bool { func DisplayJSONFormat() bool {
return outputAsJSON || outputAsJSONDebug return outputAsJSON || outputAsJSONDebug
} }

View File

@ -3,9 +3,12 @@ package backup
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
"github.com/dustin/go-humanize"
"github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/model"
@ -141,6 +144,8 @@ func New(
// CLI Output // CLI Output
// -------------------------------------------------------------------------------- // --------------------------------------------------------------------------------
// ----- print backups
// Print writes the Backup to StdOut, in the format requested by the caller. // Print writes the Backup to StdOut, in the format requested by the caller.
func (b Backup) Print(ctx context.Context) { func (b Backup) Print(ctx context.Context) {
print.Item(ctx, b) print.Item(ctx, b)
@ -162,36 +167,36 @@ func PrintAll(ctx context.Context, bs []*Backup) {
} }
type Printable struct { type Printable struct {
ID model.StableID `json:"id"` ID model.StableID `json:"id"`
ErrorCount int `json:"errorCount"` Status string `json:"status"`
StartedAt time.Time `json:"started at"` Version string `json:"version"`
Status string `json:"status"` Owner string `json:"owner"`
Version string `json:"version"` Stats backupStats `json:"stats"`
BytesRead int64 `json:"bytesRead"` }
BytesUploaded int64 `json:"bytesUploaded"`
Owner string `json:"owner"` // ToPrintable reduces the Backup to its minimally printable details.
func (b Backup) ToPrintable() Printable {
return Printable{
ID: b.ID,
Status: b.Status,
Version: "0",
Owner: b.Selector.DiscreteOwner,
Stats: b.toStats(),
}
} }
// MinimumPrintable reduces the Backup to its minimally printable details. // MinimumPrintable reduces the Backup to its minimally printable details.
func (b Backup) MinimumPrintable() any { func (b Backup) MinimumPrintable() any {
return Printable{ return b.ToPrintable()
ID: b.ID,
ErrorCount: b.ErrorCount,
StartedAt: b.StartedAt,
Status: b.Status,
Version: "0",
BytesRead: b.BytesRead,
BytesUploaded: b.BytesUploaded,
Owner: b.Selector.DiscreteOwner,
}
} }
// Headers returns the human-readable names of properties in a Backup // Headers returns the human-readable names of properties in a Backup
// for printing out to a terminal in a columnar display. // for printing out to a terminal in a columnar display.
func (b Backup) Headers() []string { func (b Backup) Headers() []string {
return []string{ return []string{
"Started At",
"ID", "ID",
"Started At",
"Duration",
"Status", "Status",
"Resource Owner", "Resource Owner",
} }
@ -255,10 +260,78 @@ func (b Backup) Values() []string {
name = b.Selector.DiscreteOwner name = b.Selector.DiscreteOwner
} }
bs := b.toStats()
return []string{ return []string{
common.FormatTabularDisplayTime(b.StartedAt),
string(b.ID), string(b.ID),
common.FormatTabularDisplayTime(b.StartedAt),
bs.EndedAt.Sub(bs.StartedAt).String(),
status, status,
name, name,
} }
} }
// ----- print backup stats
func (b Backup) toStats() backupStats {
return backupStats{
ID: string(b.ID),
BytesRead: b.BytesRead,
BytesUploaded: b.BytesUploaded,
EndedAt: b.CompletedAt,
ErrorCount: b.ErrorCount,
ItemsRead: b.ItemsRead,
ItemsSkipped: b.TotalSkippedItems,
ItemsWritten: b.ItemsWritten,
StartedAt: b.StartedAt,
}
}
// interface compliance checks
var _ print.Printable = &backupStats{}
type backupStats struct {
ID string `json:"id"`
BytesRead int64 `json:"bytesRead"`
BytesUploaded int64 `json:"bytesUploaded"`
EndedAt time.Time `json:"endedAt"`
ErrorCount int `json:"errorCount"`
ItemsRead int `json:"itemsRead"`
ItemsSkipped int `json:"itemsSkipped"`
ItemsWritten int `json:"itemsWritten"`
StartedAt time.Time `json:"startedAt"`
}
// Print writes the Backup to StdOut, in the format requested by the caller.
func (bs backupStats) Print(ctx context.Context) {
print.Item(ctx, bs)
}
// MinimumPrintable reduces the Backup to its minimally printable details.
func (bs backupStats) MinimumPrintable() any {
return bs
}
// Headers returns the human-readable names of properties in a Backup
// for printing out to a terminal in a columnar display.
func (bs backupStats) Headers() []string {
return []string{
"ID",
"Bytes Uploaded",
"Items Uploaded",
"Items Skipped",
"Errors",
}
}
// Values returns the values matching the Headers list for printing
// out to a terminal in a columnar display.
func (bs backupStats) Values() []string {
return []string{
bs.ID,
humanize.Bytes(uint64(bs.BytesUploaded)),
strconv.Itoa(bs.ItemsWritten),
strconv.Itoa(bs.ItemsSkipped),
strconv.Itoa(bs.ErrorCount),
}
}

View File

@ -1,9 +1,11 @@
package backup_test package backup_test
import ( import (
"strconv"
"testing" "testing"
"time" "time"
"github.com/dustin/go-humanize"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -50,7 +52,7 @@ func stubBackup(t time.Time, ownerID, ownerName string) backup.Backup {
}, },
StartAndEndTime: stats.StartAndEndTime{ StartAndEndTime: stats.StartAndEndTime{
StartedAt: t, StartedAt: t,
CompletedAt: t, CompletedAt: t.Add(1 * time.Minute),
}, },
SkippedCounts: stats.SkippedCounts{ SkippedCounts: stats.SkippedCounts{
TotalSkippedItems: 1, TotalSkippedItems: 1,
@ -63,22 +65,27 @@ func (suite *BackupUnitSuite) TestBackup_HeadersValues() {
var ( var (
t = suite.T() t = suite.T()
now = time.Now() now = time.Now()
later = now.Add(1 * time.Minute)
b = stubBackup(now, "id", "name") b = stubBackup(now, "id", "name")
expectHs = []string{ expectHs = []string{
"Started At",
"ID", "ID",
"Started At",
"Duration",
"Status", "Status",
"Resource Owner", "Resource Owner",
} }
nowFmt = common.FormatTabularDisplayTime(now) nowFmt = common.FormatTabularDisplayTime(now)
expectVs = []string{ expectVs = []string{
nowFmt,
"id", "id",
nowFmt,
"1m0s",
"status (2 errors, 1 skipped: 1 malware)", "status (2 errors, 1 skipped: 1 malware)",
"test", "test",
} }
) )
b.StartAndEndTime.CompletedAt = later
// single skipped malware // single skipped malware
hs := b.Headers() hs := b.Headers()
assert.Equal(t, expectHs, hs) assert.Equal(t, expectHs, hs)
@ -182,7 +189,7 @@ func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() {
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {
result := test.bup.Values() result := test.bup.Values()
assert.Equal(suite.T(), test.expect, result[2], "status value") assert.Equal(suite.T(), test.expect, result[3], "status value")
}) })
} }
} }
@ -197,10 +204,57 @@ func (suite *BackupUnitSuite) TestBackup_MinimumPrintable() {
require.True(t, ok) require.True(t, ok)
assert.Equal(t, b.ID, result.ID, "id") assert.Equal(t, b.ID, result.ID, "id")
assert.Equal(t, 2, result.ErrorCount, "error count") assert.Equal(t, 2, result.Stats.ErrorCount, "error count")
assert.Equal(t, now, result.StartedAt, "started at") assert.Equal(t, now, result.Stats.StartedAt, "started at")
assert.Equal(t, b.Status, result.Status, "status") assert.Equal(t, b.Status, result.Status, "status")
assert.Equal(t, b.BytesRead, result.BytesRead, "size") assert.Equal(t, b.BytesRead, result.Stats.BytesRead, "size")
assert.Equal(t, b.BytesUploaded, result.BytesUploaded, "stored size") assert.Equal(t, b.BytesUploaded, result.Stats.BytesUploaded, "stored size")
assert.Equal(t, b.Selector.DiscreteOwner, result.Owner, "owner") assert.Equal(t, b.Selector.DiscreteOwner, result.Owner, "owner")
} }
func (suite *BackupUnitSuite) TestStats() {
var (
t = suite.T()
start = time.Now()
b = stubBackup(start, "owner", "ownername")
s = b.ToPrintable().Stats
)
assert.Equal(t, b.BytesRead, s.BytesRead, "bytes read")
assert.Equal(t, b.BytesUploaded, s.BytesUploaded, "bytes uploaded")
assert.Equal(t, b.CompletedAt, s.EndedAt, "completion time")
assert.Equal(t, b.ErrorCount, s.ErrorCount, "error count")
assert.Equal(t, b.ItemsRead, s.ItemsRead, "items read")
assert.Equal(t, b.TotalSkippedItems, s.ItemsSkipped, "items skipped")
assert.Equal(t, b.ItemsWritten, s.ItemsWritten, "items written")
assert.Equal(t, b.StartedAt, s.StartedAt, "started at")
}
func (suite *BackupUnitSuite) TestStats_headersValues() {
var (
t = suite.T()
start = time.Now()
b = stubBackup(start, "owner", "ownername")
s = b.ToPrintable().Stats
)
expectHeaders := []string{
"ID",
"Bytes Uploaded",
"Items Uploaded",
"Items Skipped",
"Errors",
}
assert.Equal(t, expectHeaders, s.Headers())
expectValues := []string{
"id",
humanize.Bytes(uint64(b.BytesUploaded)),
strconv.Itoa(b.ItemsWritten),
strconv.Itoa(b.TotalSkippedItems),
strconv.Itoa(b.ErrorCount),
}
assert.Equal(t, expectValues, s.Values())
}

View File

@ -139,7 +139,7 @@ type DetailsModel struct {
// Print writes the DetailModel Entries to StdOut, in the format // Print writes the DetailModel Entries to StdOut, in the format
// requested by the caller. // requested by the caller.
func (dm DetailsModel) PrintEntries(ctx context.Context) { func (dm DetailsModel) PrintEntries(ctx context.Context) {
if print.JSONFormat() { if print.DisplayJSONFormat() {
printJSON(ctx, dm) printJSON(ctx, dm)
} else { } else {
printTable(ctx, dm) printTable(ctx, dm)