Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add tester framework #72

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cluerr/err.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ type Err struct {
data *node.Node
}

// Node retrieves the node values from the error.
func (err *Err) Node() *node.Node {
if isNilErrIface(err) {
return &node.Node{}
}

return err.Values()
}

// ------------------------------------------------------------
// tree operations
// ------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions internal/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (
// data nodes
// ---------------------------------------------------------------------------

type Noder interface {
Node() *Node
}

// Node contains the data tracked by both clues in contexts and in errors.
//
// These nodes compose a tree, such that nodes can walk their ancestry path from
Expand Down
293 changes: 293 additions & 0 deletions tester/tester.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
package tester

import (
"context"
"slices"

"github.com/alcionai/clues"
"github.com/alcionai/clues/cluerr"
"github.com/alcionai/clues/internal/node"
"github.com/alcionai/clues/internal/stringify"
)

// ---------------------------------------------------------------------------
// types and interfaces
// ---------------------------------------------------------------------------

type anyVal struct{}

// AnyVal will pass the test so long as the key for this value exists.
var AnyVal = anyVal{}

// AllPass will always pass the test.
var AllPass = "all labels will pass if provided this magic string"

type expectGot struct {
expect string
got string
}

// errLogfer allows us to pass in a mock testing.T
type errLogfer interface {
Error(args ...any)
Errorf(format string, args ...any)
Log(args ...any)
Logf(format string, args ...any)
}

// ---------------------------------------------------------------------------
// assertions
// ---------------------------------------------------------------------------

// Contains checks whether the errOrCtx (which should contain
// either an error or context.Context) contains the provided
// key:value pairs.
//
// Returns true if the test fails.
func Contains(
t errLogfer,
errOrCtx any,
kvs ...any,
) bool {
if slices.Contains(kvs, any(AllPass)) {
t.Log("AllPass found; passing test")
return false
}

// some sanity prechecks
if len(kvs) == 0 {
t.Error("no key:value properties provided to test")
return true
}

if len(kvs)%2 == 1 {
t.Error("odd count of key:value parameters")
return true
}

n, ok := getNode(t, errOrCtx)
if !ok {
return true
}

var (
values = n.Map()
badVals = map[string]expectGot{}
foundKeys = map[string]struct{}{}
missingKeys = map[string]struct{}{}
)

// iterate through each k,v pair looking for matches.
for i := 0; i < len(kvs); i += 2 {
k, v := stringify.Marshal(kvs[i], false), kvs[i+1]

gotV, found := values[k]
if !found {
missingKeys[k] = struct{}{}
continue
}

foundKeys[k] = struct{}{}

if v == AnyVal {
continue
}

var (
expected = stringify.Marshal(v, false)
got = stringify.Marshal(gotV, false)
)

if expected != got {
badVals[k] = expectGot{expected, got}
}
}

// early pass check
if len(badVals) == 0 && len(missingKeys) == 0 {
return false
}

showContainsResults(t, values, badVals, foundKeys, missingKeys)

return true
}

// Contains checks whether the errOrCtx (which should contain
// either an error or context.Context) contains the provided
// map.
//
// Returns true if the test fails.
func ContainsMap(
t errLogfer,
errOrCtx any,
m map[string]any,
) bool {
if len(m) == 0 {
t.Error("no map properties provided to test")
return true
}

n, ok := getNode(t, errOrCtx)
if !ok {
return true
}

var (
values = n.Map()
badVals = map[string]expectGot{}
foundKeys = map[string]struct{}{}
missingKeys = map[string]struct{}{}
)

// iterate through each k,v pair looking for matches.
for k, v := range m {
gotV, found := values[k]
if !found {
missingKeys[k] = struct{}{}
continue
}

foundKeys[k] = struct{}{}

if v == AnyVal {
continue
}

var (
expected = stringify.Marshal(v, false)
got = stringify.Marshal(gotV, false)
)

if expected != got {
badVals[k] = expectGot{expected, got}
}
}

// early pass check
if len(badVals) == 0 && len(missingKeys) == 0 {
return false
}

showContainsResults(t, values, badVals, foundKeys, missingKeys)

return true
}

// ContainsLabels checks whether the error(which should contain
// a cluerr.Err) contains the labels. If provided zero labels to
// check against, asserts that the error contains zero labels.
// Can be provided tester.AllPass to skip the check for a single
// test case.
//
// Returns true if the test fails.
func ContainsLabels(
t errLogfer,
err error,
expected ...string,
) bool {
// support an always-pass case
if slices.Contains(expected, AllPass) {
t.Log("AllPass found; passing test")
return false
}

labels := cluerr.Labels(err)

if err == nil {
if len(expected) > 0 {
t.Error("expected labels, but error is nil")
}

return len(expected) != 0
}

if len(expected) == 0 && len(labels) > 0 {
t.Errorf("expected no labels in error, got:\t%v", labels)
return true
}

extraLabels := map[string]struct{}{}

for l := range labels {
if !slices.Contains(expected, l) {
extraLabels[l] = struct{}{}
}
}

var errored bool

for _, expect := range expected {
if _, ok := labels[expect]; !ok {
t.Error("missing label:", expect)
errored = true
}
}

if errored {
t.Log("Unchecked labels")

for extra := range extraLabels {
t.Log("-", extra)
}

t.Log("")
}

return errored
}

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------

func showContainsResults(
t errLogfer,
values map[string]any,
badVals map[string]expectGot,
foundKeys map[string]struct{},
missingKeys map[string]struct{},
) {
// sanity showcase: print out all unchecked values
if len(foundKeys) < len(values) {
t.Log("Unchecked attributes")

for k, v := range values {
if _, ok := foundKeys[k]; !ok {
t.Log("-", k+":", stringify.Marshal(v, false))
}
}

t.Log("")
}

// failure showcase
for k := range missingKeys {
t.Error("missing entry with key ", k)
}

for k, eg := range badVals {
t.Errorf(
"unexpected value:\n\tkey: %s\n\texpected: %s\n\tgot: %s\n",
k,
eg.expect,
eg.got)
}
}

func getNode(
t errLogfer,
eoc any,
) (*node.Node, bool) {
if noder, ok := eoc.(node.Noder); ok {
return noder.Node(), true
}

if ctx, ok := eoc.(context.Context); ok {
return clues.In(ctx), true
}

t.Error("tester can only check error and context.Context values")

return nil, false
}
Loading
Loading