Some testing QoL functions (#4927)

Functions help check things like
  * are two items equal when taking into account nil, empty slices, and
    other zero-values?
  * is something fully populated?

---

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

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

#### Type of change

- [ ] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [x] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Test Plan

- [x] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-12-26 10:52:50 -08:00 committed by GitHub
parent 1c18131122
commit f63a6e9b4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 167 additions and 43 deletions

View File

@ -21,6 +21,7 @@ import (
"github.com/alcionai/corso/src/internal/m365/collection/drive/metadata"
odStub "github.com/alcionai/corso/src/internal/m365/service/onedrive/stub"
m365Stub "github.com/alcionai/corso/src/internal/m365/stub"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/count"
@ -160,6 +161,10 @@ func recipientEqual(
expected models.Recipientable,
got models.Recipientable,
) bool {
if expected == nil {
return tester.NilOrZero(got)
}
// Don't compare names as M365 will override the name if the address is known.
return reflect.DeepEqual(
ptr.Val(expected.GetEmailAddress().GetAddress()),
@ -173,9 +178,9 @@ func checkMessage(
) {
testElementsMatch(t, expected.GetAttachments(), got.GetAttachments(), false, attachmentEqual)
assert.Equal(t, expected.GetBccRecipients(), got.GetBccRecipients(), "BccRecipients")
tester.AssertEmptyOrEqual(t, expected.GetBccRecipients(), got.GetBccRecipients(), "BccRecipients")
assert.Equal(
tester.AssertEmptyOrEqual(
t,
ptr.Val(expected.GetBody().GetContentType()),
ptr.Val(got.GetBody().GetContentType()),
@ -187,9 +192,9 @@ func checkMessage(
// always just the first 255 characters if the message is HTML and has
// multiple paragraphs.
assert.Equal(t, expected.GetCategories(), got.GetCategories(), "Categories")
tester.AssertEmptyOrEqual(t, expected.GetCategories(), got.GetCategories(), "Categories")
assert.Equal(t, expected.GetCcRecipients(), got.GetCcRecipients(), "CcRecipients")
tester.AssertEmptyOrEqual(t, expected.GetCcRecipients(), got.GetCcRecipients(), "CcRecipients")
// Skip ChangeKey as it's tied to this specific instance of the item.
@ -202,33 +207,41 @@ func checkMessage(
checkFlags(t, expected.GetFlag(), got.GetFlag())
checkRecipentables(t, expected.GetFrom(), got.GetFrom())
assert.Equal(t, ptr.Val(expected.GetHasAttachments()), ptr.Val(got.GetHasAttachments()), "HasAttachments")
tester.AssertEmptyOrEqual(t, ptr.Val(expected.GetHasAttachments()), ptr.Val(got.GetHasAttachments()), "HasAttachments")
// Skip Id as it's tied to this specific instance of the item.
assert.Equal(t, ptr.Val(expected.GetImportance()), ptr.Val(got.GetImportance()), "Importance")
tester.AssertEmptyOrEqual(t, ptr.Val(expected.GetImportance()), ptr.Val(got.GetImportance()), "Importance")
assert.Equal(
tester.AssertEmptyOrEqual(
t,
ptr.Val(expected.GetInferenceClassification()),
ptr.Val(got.GetInferenceClassification()),
"InferenceClassification")
assert.Equal(t, expected.GetInternetMessageHeaders(), got.GetInternetMessageHeaders(), "InternetMessageHeaders")
tester.AssertEmptyOrEqual(
t,
expected.GetInternetMessageHeaders(),
got.GetInternetMessageHeaders(),
"InternetMessageHeaders")
assert.Equal(t, ptr.Val(expected.GetInternetMessageId()), ptr.Val(got.GetInternetMessageId()), "InternetMessageId")
tester.AssertEmptyOrEqual(
t,
ptr.Val(expected.GetInternetMessageId()),
ptr.Val(got.GetInternetMessageId()),
"InternetMessageId")
assert.Equal(
tester.AssertEmptyOrEqual(
t,
ptr.Val(expected.GetIsDeliveryReceiptRequested()),
ptr.Val(got.GetIsDeliveryReceiptRequested()),
"IsDeliverReceiptRequested")
assert.Equal(t, ptr.Val(expected.GetIsDraft()), ptr.Val(got.GetIsDraft()), "IsDraft")
tester.AssertEmptyOrEqual(t, ptr.Val(expected.GetIsDraft()), ptr.Val(got.GetIsDraft()), "IsDraft")
assert.Equal(t, ptr.Val(expected.GetIsRead()), ptr.Val(got.GetIsRead()), "IsRead")
tester.AssertEmptyOrEqual(t, ptr.Val(expected.GetIsRead()), ptr.Val(got.GetIsRead()), "IsRead")
assert.Equal(
tester.AssertEmptyOrEqual(
t,
ptr.Val(expected.GetIsReadReceiptRequested()),
ptr.Val(got.GetIsReadReceiptRequested()),
@ -238,21 +251,25 @@ func checkMessage(
// Skip ParentFolderId as we restore to a different folder by default.
assert.Equal(t, ptr.Val(expected.GetReceivedDateTime()), ptr.Val(got.GetReceivedDateTime()), "ReceivedDateTime")
tester.AssertEmptyOrEqual(
t,
ptr.Val(expected.GetReceivedDateTime()),
ptr.Val(got.GetReceivedDateTime()),
"ReceivedDateTime")
assert.Equal(t, expected.GetReplyTo(), got.GetReplyTo(), "ReplyTo")
tester.AssertEmptyOrEqual(t, expected.GetReplyTo(), got.GetReplyTo(), "ReplyTo")
checkRecipentables(t, expected.GetSender(), got.GetSender())
assert.Equal(t, ptr.Val(expected.GetSentDateTime()), ptr.Val(got.GetSentDateTime()), "SentDateTime")
tester.AssertEmptyOrEqual(t, ptr.Val(expected.GetSentDateTime()), ptr.Val(got.GetSentDateTime()), "SentDateTime")
assert.Equal(t, ptr.Val(expected.GetSubject()), ptr.Val(got.GetSubject()), "Subject")
tester.AssertEmptyOrEqual(t, ptr.Val(expected.GetSubject()), ptr.Val(got.GetSubject()), "Subject")
testElementsMatch(t, expected.GetToRecipients(), got.GetToRecipients(), false, recipientEqual)
// Skip WebLink as it's tied to this specific instance of the item.
assert.Equal(t, expected.GetUniqueBody(), got.GetUniqueBody(), "UniqueBody")
tester.AssertEmptyOrEqual(t, expected.GetUniqueBody(), got.GetUniqueBody(), "UniqueBody")
}
// checkFlags is a helper function to check equality of models.FollowupFlabables
@ -261,6 +278,11 @@ func checkFlags(
t *testing.T,
expected, got models.FollowupFlagable,
) {
if expected == nil {
assert.True(t, tester.NilOrZero(got))
return
}
assert.Equal(t, expected.GetCompletedDateTime(), got.GetCompletedDateTime())
assert.Equal(t, expected.GetDueDateTime(), got.GetDueDateTime())
assert.Equal(t, expected.GetFlagStatus(), got.GetFlagStatus())
@ -274,6 +296,11 @@ func checkRecipentables(
t *testing.T,
expected, got models.Recipientable,
) {
if expected == nil {
assert.True(t, tester.NilOrZero(got))
return
}
checkEmailAddressables(t, expected.GetEmailAddress(), got.GetEmailAddress())
assert.Equal(t, expected.GetAdditionalData(), got.GetAdditionalData())
}

View File

@ -3,9 +3,7 @@ package operations
import (
"context"
"encoding/json"
"fmt"
stdpath "path"
"reflect"
"testing"
"time"
@ -369,33 +367,11 @@ func TestBackupOpUnitSuite(t *testing.T) {
suite.Run(t, &BackupOpUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func checkPopulatedInner(v reflect.Value) error {
if v.IsZero() {
return clues.New("zero-valued field")
}
if v.Kind() != reflect.Struct {
return nil
}
var errs *clues.Err
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if err := checkPopulatedInner(f); err != nil {
errs = clues.Stack(errs, clues.Wrap(err, fmt.Sprintf("field at index %d", i)))
}
}
return errs.OrNil()
}
// checkPopulated ensures that input has no zero-valued fields. That helps
// ensure that even as future updates to input happen in other files the changes
// are propagated here due to test failures.
func checkPopulated(t *testing.T, input control.Options) {
err := checkPopulatedInner(reflect.ValueOf(input))
err := tester.CheckPopulated(input)
require.NoError(t, err, clues.ToCore(err))
}

View File

@ -0,0 +1,121 @@
package tester
import (
"fmt"
"reflect"
"testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert"
)
func checkPopulated(v reflect.Value) error {
if v.IsZero() {
return clues.New("zero-valued field")
}
if v.Kind() != reflect.Struct {
return nil
}
var errs *clues.Err
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if err := checkPopulated(f); err != nil {
errs = clues.Stack(errs, clues.Wrap(err, fmt.Sprintf("field at index %d", i)))
}
}
return errs.OrNil()
}
func isEmptyContainer(v reflect.Value) bool {
// Handle pointers to things.
deref := v
for k := deref.Kind(); k == reflect.Pointer; k = deref.Kind() {
deref = deref.Elem()
}
// Check for empty maps, slices, or arrays.
if (deref.Kind() == reflect.Slice && deref.Len() == 0) ||
(deref.Kind() == reflect.Map && deref.Len() == 0) ||
(deref.Kind() == reflect.Array && deref.Len() == 0) {
return true
}
return false
}
// CheckPopulated returns an error if input is not fully populated. To be
// considered fully populated it must be non-zero-valued. For basic types this
// just means it isn't the zero value. For structs this means that every field
// is not zero-valued. This check is recursive for structs.
func CheckPopulated(input any) error {
return checkPopulated(reflect.ValueOf(input))
}
func checkNotPopulated(v reflect.Value) error {
if isEmptyContainer(v) {
return nil
}
if !v.IsZero() {
return clues.New("non-zero-valued field")
}
if v.Kind() != reflect.Struct {
return nil
}
var errs *clues.Err
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if err := checkNotPopulated(f); err != nil {
errs = clues.Stack(errs, clues.Wrap(err, fmt.Sprintf("field at index %d", i)))
}
}
return errs.OrNil()
}
// NilOrZero return true if the input is nil or if it's the zero value for the
// type. If the input is a struct then all fields are recursively checked to
// see if they're the zero value for their type.
func NilOrZero(input any) bool {
if input == nil {
return true
}
if isEmptyContainer(reflect.ValueOf(input)) {
return true
}
err := checkNotPopulated(reflect.ValueOf(input))
return err != nil
}
// AssertEmptyOrEqual checks either:
// - got is nil or the zero value if expected is nil
// - expected and got are equal if expected is not nil
func AssertEmptyOrEqual(
t *testing.T,
expect any,
got any,
msgAndArgs ...any,
) bool {
if expect == nil {
return assert.True(t, NilOrZero(got), "empty got value: %+v", got)
}
if isEmptyContainer(reflect.ValueOf(expect)) {
return assert.True(t, NilOrZero(got), "empty got value: %+v", got)
}
return assert.Equal(t, expect, got, msgAndArgs...)
}