Change printing of backup stats to single line (#4526)

Previously we were printing a table for a single item which, for one
was unnecessary, but this also make the output of a backup more
compact.

Before:

```
Logging to file: /home/meain/.cache/corso/logs/2023-10-20T10-21-23Z.log
Connecting to M365                               done
Connecting to repository                      1s done

Backing Up ∙ Teams Testing (meain)
Discovering items to backup                  27s done
    Libraries (TeamsTestingmeain)                  done (found 0 items)
    Libraries (TeamsTestingmeain-Shared0)          done (found 0 items)
    Libraries (TeamsTestingmeain-Private2)         done (found 0 items)
    Messages                                    7s done (found 6 channels)
Backing up data                               7s done
Backup complete ∙ 7b40dd40-f808-4d57-8e39-b4553e48dc5d
    ID                                    Bytes Uploaded  Items Uploaded  Items Skipped  Errors
    7b40dd40-f808-4d57-8e39-b4553e48dc5d  0 B             0               0              0

Completed Backups:
    ID                                    Started At            Duration       Status     Resource Owner
    7b40dd40-f808-4d57-8e39-b4553e48dc5d  2023-10-20T10:21:32Z  36.632747912s  Completed  Teams Testing (meain)
```

After:
```
Connecting to M365                               done
Connecting to repository                      1s done

Backing Up ∙ Teams Testing (meain)
Discovering items to backup                  31s done
    Libraries (TeamsTestingmeain)                  done (found 0 items)
    Libraries (TeamsTestingmeain-Shared0)          done (found 0 items)
    Libraries (TeamsTestingmeain-Private2)         done (found 0 items)
    Messages                                    9s done (found 6 channels)
Backing up data                               7s done
Backup complete ∙ ffb2f619-1cb7-4a11-b3e2-7300aa513c6a
Bytes Uploaded: 0 B | Items Uploaded: 0 | Items Skipped: 0 | Errors: 0

Completed Backups:
    ID                                    ID                    Started At     Duration   Status                 Resource Owner
    ffb2f619-1cb7-4a11-b3e2-7300aa513c6a  2023-10-20T10:23:35Z  40.096203016s  Completed  Teams Testing (meain)
```


---

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

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abin Simon 2023-11-16 18:13:27 +05:30 committed by GitHub
parent b46f242bc4
commit 68256cf59f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 180 additions and 61 deletions

View File

@ -9,13 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added export support for emails in exchange backups as `.eml` files - Added export support for emails in exchange backups as `.eml` files
- More colorful and informational cli output
### Changed ### Changed
- Change file extension of messages export to json to match the content - Change file extension of messages export to json to match the content
### Fixed ### Fixed
- Handle OneDrive folders being deleted and recreated midway through a backup - Handle OneDrive folders being deleted and recreated midway through a backup
- Automatically re-run a full delta query on incrmental if the prior backup is found to have malformed prior-state information. - Automatically re-run a full delta query on incremental if the prior backup is found to have malformed prior-state information.
- Retry drive item permission downloads during long-running backups after the jwt token expires and refreshes. - Retry drive item permission downloads during long-running backups after the jwt token expires and refreshes.
## [v0.15.0] (beta) - 2023-10-31 ## [v0.15.0] (beta) - 2023-10-31

View File

@ -247,7 +247,7 @@ func genericCreateCommand(
} }
if len(bups) > 0 { if len(bups) > 0 {
Info(ctx, "Completed Backups:") Info(ctx, "\nCompleted Backups:")
backup.PrintAll(ctx, bups) backup.PrintAll(ctx, bups)
} }
@ -420,6 +420,5 @@ func printBackupStats(ctx context.Context, r repository.Repositoryer, bid string
logger.CtxErr(ctx, err).Error("finding backup immediately after backup operation completion") logger.CtxErr(ctx, err).Error("finding backup immediately after backup operation completion")
} }
b.ToPrintable().Stats.Print(ctx) b.ToPrintable().Stats.PrintProperties(ctx)
Info(ctx, " ")
} }

View File

@ -46,11 +46,12 @@ func (ev envVar) MinimumPrintable() any {
return ev return ev
} }
func (ev envVar) Headers() []string { func (ev envVar) Headers(bool) []string {
// NOTE: skipID does not make sense in this context
return []string{ev.category, " "} return []string{ev.category, " "}
} }
func (ev envVar) Values() []string { func (ev envVar) Values(bool) []string {
return []string{ev.name, ev.description} return []string{ev.name, ev.description}
} }

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tidwall/pretty" "github.com/tidwall/pretty"
@ -178,11 +179,11 @@ func outf(ctx context.Context, w io.Writer, t string, s ...any) {
type Printable interface { type Printable interface {
minimumPrintabler minimumPrintabler
// should list the property names of the values surfaced in Values() // should list the property names of the values surfaced in Values()
Headers() []string Headers(skipID bool) []string
// list of values for tabular or csv formatting // list of values for tabular or csv formatting
// if the backing data is nil or otherwise missing, // if the backing data is nil or otherwise missing,
// values should provide an empty string as opposed to skipping entries // values should provide an empty string as opposed to skipping entries
Values() []string Values(skipID bool) []string
} }
type minimumPrintabler interface { type minimumPrintabler interface {
@ -206,6 +207,23 @@ func printItem(w io.Writer, p Printable) {
outputTable(w, []Printable{p}) outputTable(w, []Printable{p})
} }
// ItemProperties prints the printable either as in a single line or a json
// The difference between this and Item is that this one does not print the ID
func ItemProperties(ctx context.Context, p Printable) {
printItemProperties(getRootCmd(ctx).OutOrStdout(), p)
}
// print prints the printable items,
// according to the caller's requested format.
func printItemProperties(w io.Writer, p Printable) {
if outputAsJSON || outputAsJSONDebug {
outputJSON(w, p, outputAsJSONDebug)
return
}
outputOneLine(w, []Printable{p})
}
// All prints the slice of printable items, // All prints the slice of printable items,
// according to the caller's requested format. // according to the caller's requested format.
func All(ctx context.Context, ps ...Printable) { func All(ctx context.Context, ps ...Printable) {
@ -240,12 +258,12 @@ func Table(ctx context.Context, ps []Printable) {
// output to stdout the list of printable structs in a table // output to stdout the list of printable structs in a table
func outputTable(w io.Writer, ps []Printable) { func outputTable(w io.Writer, ps []Printable) {
t := table.Table{ t := table.Table{
Headers: ps[0].Headers(), Headers: ps[0].Headers(false),
Rows: [][]string{}, Rows: [][]string{},
} }
for _, p := range ps { for _, p := range ps {
t.Rows = append(t.Rows, p.Values()) t.Rows = append(t.Rows, p.Values(false))
} }
_ = t.WriteTable( _ = t.WriteTable(
@ -309,3 +327,28 @@ func printPrettyJSON(w io.Writer, a any) {
fmt.Fprintln(w, string(pretty.Pretty(bs))) fmt.Fprintln(w, string(pretty.Pretty(bs)))
} }
// -------------------------------------------------------------------------------------------
// One line
// -------------------------------------------------------------------------------------------
// Output in the following format:
// Bytes Uploaded: 401 kB | Items Uploaded: 59 | Items Skipped: 0 | Errors: 0
func outputOneLine(w io.Writer, ps []Printable) {
headers := ps[0].Headers(true)
rows := [][]string{}
for _, p := range ps {
rows = append(rows, p.Values(true))
}
printables := []string{}
for _, row := range rows {
for i, col := range row {
printables = append(printables, fmt.Sprintf("%s: %s", headers[i], col))
}
}
fmt.Fprintln(w, strings.Join(printables, " | "))
}

View File

@ -173,6 +173,12 @@ func (b Backup) Print(ctx context.Context) {
print.Item(ctx, b) print.Item(ctx, b)
} }
// PrintProperties writes the Backup to StdOut, in the format requested by the caller.
// Unlike Print, it skips the ID of the Backup
func (b Backup) PrintProperties(ctx context.Context) {
print.ItemProperties(ctx, b)
}
// PrintAll writes the slice of Backups to StdOut, in the format requested by the caller. // PrintAll writes the slice of Backups to StdOut, in the format requested by the caller.
func PrintAll(ctx context.Context, bs []*Backup) { func PrintAll(ctx context.Context, bs []*Backup) {
if len(bs) == 0 { if len(bs) == 0 {
@ -218,19 +224,24 @@ func (b Backup) MinimumPrintable() any {
// 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(skipID bool) []string {
return []string{ headers := []string{
"ID",
"Started At", "Started At",
"Duration", "Duration",
"Status", "Status",
"Resource Owner", "Resource Owner",
} }
if skipID {
return headers
}
return append([]string{"ID"}, headers...)
} }
// Values returns the values matching the Headers list for printing // Values returns the values matching the Headers list for printing
// out to a terminal in a columnar display. // out to a terminal in a columnar display.
func (b Backup) Values() []string { func (b Backup) Values(skipID bool) []string {
var ( var (
status = b.Status status = b.Status
errCount = b.ErrorCount errCount = b.ErrorCount
@ -281,13 +292,18 @@ func (b Backup) Values() []string {
bs := b.toStats() bs := b.toStats()
return []string{ values := []string{
string(b.ID),
dttm.FormatToTabularDisplay(b.StartedAt), dttm.FormatToTabularDisplay(b.StartedAt),
bs.EndedAt.Sub(bs.StartedAt).String(), bs.EndedAt.Sub(bs.StartedAt).String(),
status, status,
name, name,
} }
if skipID {
return values
}
return append([]string{string(b.ID)}, values...)
} }
// ----- print backup stats // ----- print backup stats
@ -326,6 +342,12 @@ func (bs backupStats) Print(ctx context.Context) {
print.Item(ctx, bs) print.Item(ctx, bs)
} }
// PrintProperties writes the Backup to StdOut, in the format requested by the caller.
// Unlike Print, it skips the ID of backupStats
func (bs backupStats) PrintProperties(ctx context.Context) {
print.ItemProperties(ctx, bs)
}
// MinimumPrintable reduces the Backup to its minimally printable details. // MinimumPrintable reduces the Backup to its minimally printable details.
func (bs backupStats) MinimumPrintable() any { func (bs backupStats) MinimumPrintable() any {
return bs return bs
@ -333,24 +355,34 @@ func (bs backupStats) MinimumPrintable() any {
// 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 (bs backupStats) Headers() []string { func (bs backupStats) Headers(skipID bool) []string {
return []string{ headers := []string{
"ID",
"Bytes Uploaded", "Bytes Uploaded",
"Items Uploaded", "Items Uploaded",
"Items Skipped", "Items Skipped",
"Errors", "Errors",
} }
if skipID {
return headers
}
return append([]string{"ID"}, headers...)
} }
// Values returns the values matching the Headers list for printing // Values returns the values matching the Headers list for printing
// out to a terminal in a columnar display. // out to a terminal in a columnar display.
func (bs backupStats) Values() []string { func (bs backupStats) Values(skipID bool) []string {
return []string{ values := []string{
bs.ID,
humanize.Bytes(uint64(bs.BytesUploaded)), humanize.Bytes(uint64(bs.BytesUploaded)),
strconv.Itoa(bs.ItemsWritten), strconv.Itoa(bs.ItemsWritten),
strconv.Itoa(bs.ItemsSkipped), strconv.Itoa(bs.ItemsSkipped),
strconv.Itoa(bs.ErrorCount), strconv.Itoa(bs.ErrorCount),
} }
if skipID {
return values
}
return append([]string{bs.ID}, values...)
} }

View File

@ -173,11 +173,17 @@ func (suite *BackupUnitSuite) TestBackup_HeadersValues() {
b.StartAndEndTime.CompletedAt = later b.StartAndEndTime.CompletedAt = later
// single skipped malware // single skipped malware
hs := b.Headers() hs := b.Headers(false)
assert.Equal(t, expectHs, hs) assert.Equal(t, expectHs, hs)
vs := b.Values() vs := b.Values(false)
assert.Equal(t, expectVs, vs) assert.Equal(t, expectVs, vs)
hs = b.Headers(true)
assert.Equal(t, expectHs[1:], hs)
vs = b.Values(true)
assert.Equal(t, expectVs[1:], vs)
} }
func (suite *BackupUnitSuite) TestBackup_HeadersValues_onlyResourceOwners() { func (suite *BackupUnitSuite) TestBackup_HeadersValues_onlyResourceOwners() {
@ -209,11 +215,17 @@ func (suite *BackupUnitSuite) TestBackup_HeadersValues_onlyResourceOwners() {
b.StartAndEndTime.CompletedAt = later b.StartAndEndTime.CompletedAt = later
// single skipped malware // single skipped malware
hs := b.Headers() hs := b.Headers(false)
assert.Equal(t, expectHs, hs) assert.Equal(t, expectHs, hs)
vs := b.Values() vs := b.Values(false)
assert.Equal(t, expectVs, vs) assert.Equal(t, expectVs, vs)
hs = b.Headers(true)
assert.Equal(t, expectHs[1:], hs)
vs = b.Values(true)
assert.Equal(t, expectVs[1:], vs)
} }
func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() { func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() {
@ -297,8 +309,11 @@ 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(false)
assert.Equal(suite.T(), test.expect, result[3], "status value") assert.Equal(suite.T(), test.expect, result[3], "status value")
result = test.bup.Values(true)
assert.Equal(suite.T(), test.expect, result[2], "status value")
}) })
} }
} }
@ -355,7 +370,8 @@ func (suite *BackupUnitSuite) TestStats_headersValues() {
"Errors", "Errors",
} }
assert.Equal(t, expectHeaders, s.Headers()) assert.Equal(t, expectHeaders, s.Headers(false))
assert.Equal(t, expectHeaders[1:], s.Headers(true))
expectValues := []string{ expectValues := []string{
"id", "id",
@ -365,5 +381,6 @@ func (suite *BackupUnitSuite) TestStats_headersValues() {
strconv.Itoa(b.ErrorCount), strconv.Itoa(b.ErrorCount),
} }
assert.Equal(t, expectValues, s.Values()) assert.Equal(t, expectValues, s.Values(false))
assert.Equal(t, expectValues[1:], s.Values(true))
} }

View File

@ -174,10 +174,15 @@ func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
hs := test.entry.Headers() hs := test.entry.Headers(false)
assert.Equal(t, test.expectHs, hs) assert.Equal(t, test.expectHs, hs)
vs := test.entry.Values() vs := test.entry.Values(false)
assert.Equal(t, test.expectVs, vs) assert.Equal(t, test.expectVs, vs)
hs = test.entry.Headers(true)
assert.Equal(t, test.expectHs[1:], hs)
vs = test.entry.Values(true)
assert.Equal(t, test.expectVs[1:], vs)
}) })
} }
} }

View File

@ -140,55 +140,63 @@ func (de Entry) MinimumPrintable() any {
// Headers returns the human-readable names of properties in a DetailsEntry // Headers returns the human-readable names of properties in a DetailsEntry
// for printing out to a terminal in a columnar display. // for printing out to a terminal in a columnar display.
func (de Entry) Headers() []string { func (de Entry) Headers(skipID bool) []string {
hs := []string{"ID"} hs := []string{}
if de.ItemInfo.Folder != nil { if de.ItemInfo.Folder != nil {
hs = append(hs, de.ItemInfo.Folder.Headers()...) hs = de.ItemInfo.Folder.Headers()
} }
if de.ItemInfo.Exchange != nil { if de.ItemInfo.Exchange != nil {
hs = append(hs, de.ItemInfo.Exchange.Headers()...) hs = de.ItemInfo.Exchange.Headers()
} }
if de.ItemInfo.SharePoint != nil { if de.ItemInfo.SharePoint != nil {
hs = append(hs, de.ItemInfo.SharePoint.Headers()...) hs = de.ItemInfo.SharePoint.Headers()
} }
if de.ItemInfo.OneDrive != nil { if de.ItemInfo.OneDrive != nil {
hs = append(hs, de.ItemInfo.OneDrive.Headers()...) hs = de.ItemInfo.OneDrive.Headers()
} }
if de.ItemInfo.Groups != nil { if de.ItemInfo.Groups != nil {
hs = append(hs, de.ItemInfo.Groups.Headers()...) hs = de.ItemInfo.Groups.Headers()
} }
return hs if skipID {
return hs
}
return append([]string{"ID"}, hs...)
} }
// Values returns the values matching the Headers list. // Values returns the values matching the Headers list.
func (de Entry) Values() []string { func (de Entry) Values(skipID bool) []string {
vs := []string{de.ShortRef} vs := []string{}
if de.ItemInfo.Folder != nil { if de.ItemInfo.Folder != nil {
vs = append(vs, de.ItemInfo.Folder.Values()...) vs = de.ItemInfo.Folder.Values()
} }
if de.ItemInfo.Exchange != nil { if de.ItemInfo.Exchange != nil {
vs = append(vs, de.ItemInfo.Exchange.Values()...) vs = de.ItemInfo.Exchange.Values()
} }
if de.ItemInfo.SharePoint != nil { if de.ItemInfo.SharePoint != nil {
vs = append(vs, de.ItemInfo.SharePoint.Values()...) vs = de.ItemInfo.SharePoint.Values()
} }
if de.ItemInfo.OneDrive != nil { if de.ItemInfo.OneDrive != nil {
vs = append(vs, de.ItemInfo.OneDrive.Values()...) vs = de.ItemInfo.OneDrive.Values()
} }
if de.ItemInfo.Groups != nil { if de.ItemInfo.Groups != nil {
vs = append(vs, de.ItemInfo.Groups.Values()...) vs = de.ItemInfo.Groups.Values()
} }
return vs if skipID {
return vs
}
return append([]string{de.ShortRef}, vs...)
} }

View File

@ -42,12 +42,13 @@ func (a Alert) MinimumPrintable() any {
// Headers returns the human-readable names of properties of a skipped Item // Headers returns the human-readable names of properties of a skipped Item
// for printing out to a terminal. // for printing out to a terminal.
func (a Alert) Headers() []string { func (a Alert) Headers(bool) []string {
// NOTE: skipID does not make sense in this context and is skipped
return []string{"Action", "Message", "Container", "Name", "ID"} return []string{"Action", "Message", "Container", "Name", "ID"}
} }
// Values populates the printable values matching the Headers list. // Values populates the printable values matching the Headers list.
func (a Alert) Values() []string { func (a Alert) Values(bool) []string {
var cn string var cn string
acn, ok := a.Item.Additional[AddtlContainerName] acn, ok := a.Item.Additional[AddtlContainerName]

View File

@ -81,8 +81,11 @@ func (suite *AlertUnitSuite) TestAlert_HeadersValues() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
assert.Equal(t, []string{"Action", "Message", "Container", "Name", "ID"}, test.alert.Headers()) assert.Equal(t, []string{"Action", "Message", "Container", "Name", "ID"}, test.alert.Headers(false))
assert.Equal(t, test.expect, test.alert.Values()) assert.Equal(t, test.expect, test.alert.Values(false))
assert.Equal(t, []string{"Action", "Message", "Container", "Name", "ID"}, test.alert.Headers(true))
assert.Equal(t, test.expect, test.alert.Values(true))
}) })
} }
} }

View File

@ -491,11 +491,12 @@ func (pec printableErrCore) MinimumPrintable() any {
return pec return pec
} }
func (pec printableErrCore) Headers() []string { func (pec printableErrCore) Headers(bool) []string {
// NOTE: skipID does not make sense in this context
return []string{"Error"} return []string{"Error"}
} }
func (pec printableErrCore) Values() []string { func (pec printableErrCore) Values(bool) []string {
if pec.ErrCore == nil { if pec.ErrCore == nil {
return []string{"<nil>"} return []string{"<nil>"}
} }

View File

@ -103,12 +103,13 @@ func (i Item) MinimumPrintable() any {
// Headers returns the human-readable names of properties of an Item // Headers returns the human-readable names of properties of an Item
// for printing out to a terminal. // for printing out to a terminal.
func (i Item) Headers() []string { func (i Item) Headers(bool) []string {
// NOTE: skipID does not make sense in this context
return []string{"Action", "Type", "Name", "Container", "Cause"} return []string{"Action", "Type", "Name", "Container", "Cause"}
} }
// Values populates the printable values matching the Headers list. // Values populates the printable values matching the Headers list.
func (i Item) Values() []string { func (i Item) Values(bool) []string {
var cn string var cn string
acn, ok := i.Additional[AddtlContainerName] acn, ok := i.Additional[AddtlContainerName]

View File

@ -149,8 +149,11 @@ func (suite *ItemUnitSuite) TestItem_HeadersValues() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.item.Headers()) assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.item.Headers(false))
assert.Equal(t, test.expect, test.item.Values()) assert.Equal(t, test.expect, test.item.Values(false))
assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.item.Headers(true))
assert.Equal(t, test.expect, test.item.Values(true))
}) })
} }
} }

View File

@ -76,12 +76,13 @@ func (s Skipped) MinimumPrintable() any {
// Headers returns the human-readable names of properties of a skipped Item // Headers returns the human-readable names of properties of a skipped Item
// for printing out to a terminal. // for printing out to a terminal.
func (s Skipped) Headers() []string { func (s Skipped) Headers(bool) []string {
// NOTE: skipID does not make sense in this context
return []string{"Action", "Type", "Name", "Container", "Cause"} return []string{"Action", "Type", "Name", "Container", "Cause"}
} }
// Values populates the printable values matching the Headers list. // Values populates the printable values matching the Headers list.
func (s Skipped) Values() []string { func (s Skipped) Values(bool) []string {
var cn string var cn string
acn, ok := s.Item.Additional[AddtlContainerName] acn, ok := s.Item.Additional[AddtlContainerName]

View File

@ -120,8 +120,11 @@ func (suite *SkippedUnitSuite) TestSkipped_HeadersValues() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.skip.Headers()) assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.skip.Headers(false))
assert.Equal(t, test.expect, test.skip.Values()) assert.Equal(t, test.expect, test.skip.Values(false))
assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.skip.Headers(true))
assert.Equal(t, test.expect, test.skip.Values(true))
}) })
} }
} }