From 8d52f7655a5c851c283f15e88c9e971e25e13481 Mon Sep 17 00:00:00 2001 From: ryanfkeepers Date: Mon, 20 Feb 2023 15:59:33 -0700 Subject: [PATCH] Add new graph status in prep for migration Fault allows us to remove the error tracking from the graph support statuses. This PR is a first step in a small refactor to slim down the status. Following PRs will replace the existing status with the new one. --- .../graph/status/operation_string.go | 25 +++++ src/internal/connector/graph/status/status.go | 104 ++++++++++++++++++ .../connector/graph/status/status_test.go | 91 +++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/internal/connector/graph/status/operation_string.go create mode 100644 src/internal/connector/graph/status/status.go create mode 100644 src/internal/connector/graph/status/status_test.go diff --git a/src/internal/connector/graph/status/operation_string.go b/src/internal/connector/graph/status/operation_string.go new file mode 100644 index 000000000..e148052d7 --- /dev/null +++ b/src/internal/connector/graph/status/operation_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=Operation"; DO NOT EDIT. + +package status + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[OpUnknown-0] + _ = x[Backup-1] + _ = x[Restore-2] +} + +const _Operation_name = "OpUnknownBackupRestore" + +var _Operation_index = [...]uint8{0, 9, 15, 22} + +func (i Operation) String() string { + if i < 0 || i >= Operation(len(_Operation_index)-1) { + return "Operation(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Operation_name[_Operation_index[i]:_Operation_index[i+1]] +} diff --git a/src/internal/connector/graph/status/status.go b/src/internal/connector/graph/status/status.go new file mode 100644 index 000000000..92959420e --- /dev/null +++ b/src/internal/connector/graph/status/status.go @@ -0,0 +1,104 @@ +package status + +import ( + "context" + "fmt" + + "github.com/dustin/go-humanize" +) + +type Operation int + +//go:generate stringer -type=Operation +const ( + OpUnknown Operation = iota + Backup + Restore +) + +// ConnectorStatus is a data type used to describe the state of the sequence of operations. +type ConnectorStatus struct { + Metrics Counts + + details string + incomplete bool + op Operation +} + +type Counts struct { + Bytes int64 + Folders, Objects, Successes int +} + +func CombineCounts(a, b Counts) Counts { + return Counts{ + Bytes: a.Bytes + b.Bytes, + Folders: a.Folders + b.Folders, + Objects: a.Objects + b.Objects, + Successes: a.Successes + b.Successes, + } +} + +// Constructor for ConnectorStatus. If the counts do not agree, an error is returned. +func New( + ctx context.Context, + op Operation, + cs Counts, + details string, + incomplete bool, +) ConnectorStatus { + status := ConnectorStatus{ + Metrics: cs, + details: details, + incomplete: incomplete, + op: op, + } + + return status +} + +// Combine aggregates both ConnectorStatus value into a single status. +func Combine(one, two ConnectorStatus) ConnectorStatus { + if one.op == OpUnknown { + return two + } + + if two.op == OpUnknown { + return one + } + + status := ConnectorStatus{ + Metrics: CombineCounts(one.Metrics, two.Metrics), + details: one.details + ", " + two.details, + incomplete: one.incomplete || two.incomplete, + op: one.op, + } + + return status +} + +func (cos ConnectorStatus) String() string { + var operationStatement string + + switch cos.op { + case Backup: + operationStatement = "Downloaded from " + case Restore: + operationStatement = "Restored content to " + } + + var incomplete string + if cos.incomplete { + incomplete = "Incomplete " + } + + message := fmt.Sprintf("%sAction: %s performed on %d of %d objects (%s) within %d directories.", + incomplete, + cos.op.String(), + cos.Metrics.Successes, + cos.Metrics.Objects, + humanize.Bytes(uint64(cos.Metrics.Bytes)), + cos.Metrics.Folders) + + return message + " " + operationStatement + cos.details +} diff --git a/src/internal/connector/graph/status/status_test.go b/src/internal/connector/graph/status/status_test.go new file mode 100644 index 000000000..923991314 --- /dev/null +++ b/src/internal/connector/graph/status/status_test.go @@ -0,0 +1,91 @@ +package status + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/tester" +) + +type StatusUnitSuite struct { + tester.Suite +} + +func TestGraphConnectorStatus(t *testing.T) { + suite.Run(t, &StatusUnitSuite{tester.NewUnitSuite(t)}) +} + +func (suite *StatusUnitSuite) TestNew() { + ctx, flush := tester.NewContext() + defer flush() + + result := New( + ctx, + Backup, + Counts{1, 1, 1, 1}, + "details", + true) + assert.True(suite.T(), result.incomplete, "status is incomplete") +} + +func (suite *StatusUnitSuite) TestMergeStatus() { + ctx, flush := tester.NewContext() + defer flush() + + table := []struct { + name string + one ConnectorStatus + two ConnectorStatus + expectOP Operation + expected Counts + isIncomplete assert.BoolAssertionFunc + }{ + { + name: "Test: Status + unknown", + one: New(ctx, Backup, Counts{1, 1, 1, 1}, "details", false), + two: ConnectorStatus{}, + expectOP: Backup, + expected: Counts{1, 1, 1, 1}, + isIncomplete: assert.False, + }, + { + name: "Test: unknown + Status", + one: ConnectorStatus{}, + two: New(ctx, Backup, Counts{1, 1, 1, 1}, "details", false), + expectOP: Backup, + expected: Counts{1, 1, 1, 1}, + isIncomplete: assert.False, + }, + { + name: "Test: complete + complete", + one: New(ctx, Backup, Counts{1, 1, 3, 0}, "details", false), + two: New(ctx, Backup, Counts{3, 3, 3, 0}, "details", false), + expectOP: Backup, + expected: Counts{4, 4, 6, 0}, + isIncomplete: assert.False, + }, + { + name: "Test: complete + incomplete", + one: New(ctx, Restore, Counts{17, 17, 13, 0}, "details", false), + two: New(ctx, Restore, Counts{12, 9, 8, 0}, "details", true), + expectOP: Restore, + expected: Counts{29, 26, 21, 0}, + isIncomplete: assert.True, + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + result := Combine(test.one, test.two) + assert.Equal(t, result.op, test.expectOP) + assert.Equal(t, test.expected.Folders, result.Metrics.Folders) + assert.Equal(t, test.expected.Objects, result.Metrics.Objects) + assert.Equal(t, test.expected.Successes, result.Metrics.Successes) + assert.Equal(t, test.expected.Bytes, result.Metrics.Bytes) + test.isIncomplete(t, result.incomplete) + }) + } +}