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,8 +230,14 @@ func runBackups(
}
bIDs = append(bIDs, string(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)
if berrs.Failure() != nil {
@ -335,3 +341,13 @@ func getAccountAndConnect(ctx context.Context) (repository.Repository, *account.
func ifShow(flag string) bool {
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"))
}
// JSONFormat returns true if the printer plans to output as json.
func JSONFormat() bool {
// DisplayJSONFormat returns true if the printer plans to output as json.
func DisplayJSONFormat() bool {
return outputAsJSON || outputAsJSONDebug
}

View File

@ -3,9 +3,12 @@ package backup
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/model"
@ -141,6 +144,8 @@ func New(
// CLI Output
// --------------------------------------------------------------------------------
// ----- print backups
// Print writes the Backup to StdOut, in the format requested by the caller.
func (b Backup) Print(ctx context.Context) {
print.Item(ctx, b)
@ -163,35 +168,35 @@ func PrintAll(ctx context.Context, bs []*Backup) {
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"`
BytesRead int64 `json:"bytesRead"`
BytesUploaded int64 `json:"bytesUploaded"`
Owner string `json:"owner"`
Stats backupStats `json:"stats"`
}
// 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.
func (b Backup) MinimumPrintable() any {
return Printable{
ID: b.ID,
ErrorCount: b.ErrorCount,
StartedAt: b.StartedAt,
Status: b.Status,
Version: "0",
BytesRead: b.BytesRead,
BytesUploaded: b.BytesUploaded,
Owner: b.Selector.DiscreteOwner,
}
return b.ToPrintable()
}
// 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",
"Started At",
"Duration",
"Status",
"Resource Owner",
}
@ -255,10 +260,78 @@ func (b Backup) Values() []string {
name = b.Selector.DiscreteOwner
}
bs := b.toStats()
return []string{
common.FormatTabularDisplayTime(b.StartedAt),
string(b.ID),
common.FormatTabularDisplayTime(b.StartedAt),
bs.EndedAt.Sub(bs.StartedAt).String(),
status,
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
import (
"strconv"
"testing"
"time"
"github.com/dustin/go-humanize"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -50,7 +52,7 @@ func stubBackup(t time.Time, ownerID, ownerName string) backup.Backup {
},
StartAndEndTime: stats.StartAndEndTime{
StartedAt: t,
CompletedAt: t,
CompletedAt: t.Add(1 * time.Minute),
},
SkippedCounts: stats.SkippedCounts{
TotalSkippedItems: 1,
@ -63,22 +65,27 @@ func (suite *BackupUnitSuite) TestBackup_HeadersValues() {
var (
t = suite.T()
now = time.Now()
later = now.Add(1 * time.Minute)
b = stubBackup(now, "id", "name")
expectHs = []string{
"Started At",
"ID",
"Started At",
"Duration",
"Status",
"Resource Owner",
}
nowFmt = common.FormatTabularDisplayTime(now)
expectVs = []string{
nowFmt,
"id",
nowFmt,
"1m0s",
"status (2 errors, 1 skipped: 1 malware)",
"test",
}
)
b.StartAndEndTime.CompletedAt = later
// single skipped malware
hs := b.Headers()
assert.Equal(t, expectHs, hs)
@ -182,7 +189,7 @@ func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() {
for _, test := range table {
suite.Run(test.name, func() {
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)
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, 2, result.Stats.ErrorCount, "error count")
assert.Equal(t, now, result.Stats.StartedAt, "started at")
assert.Equal(t, b.Status, result.Status, "status")
assert.Equal(t, b.BytesRead, result.BytesRead, "size")
assert.Equal(t, b.BytesUploaded, result.BytesUploaded, "stored size")
assert.Equal(t, b.BytesRead, result.Stats.BytesRead, "size")
assert.Equal(t, b.BytesUploaded, result.Stats.BytesUploaded, "stored size")
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
// requested by the caller.
func (dm DetailsModel) PrintEntries(ctx context.Context) {
if print.JSONFormat() {
if print.DisplayJSONFormat() {
printJSON(ctx, dm)
} else {
printTable(ctx, dm)