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

[v17] add emptiness testutils #50878

Merged
merged 1 commit into from
Jan 9, 2025
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
2 changes: 1 addition & 1 deletion lib/autoupdate/agent/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestUpdater_Disable(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/client/db/postgres/repl/repl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import (
clientproto "github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/lib/client/db/postgres/repl/testdata"
dbrepl "github.com/gravitational/teleport/lib/client/db/repl"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestStart(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/config/openssh/openssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestWriteSSHConfig(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/config/systemd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestWriteSystemdUnitFile(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/access_graph_aws_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestAccessGraphIAMConfigReqDefaults(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/aws_app_access_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestAWSAppAccessConfigReqDefaults(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/deployservice_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/integrations/awsoidc/tags"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

var badParameterCheck = func(t require.TestingT, err error, msgAndArgs ...interface{}) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/ec2_ssm_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestEC2SSMIAMConfigReqDefaults(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/eice_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestEICEIAMConfigReqDefaults(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/eks_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestEKSIAMConfigReqDefaults(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/idp_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import (
"github.com/gravitational/teleport/lib"
awslib "github.com/gravitational/teleport/lib/cloud/aws"
"github.com/gravitational/teleport/lib/integrations/awsoidc/tags"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestIdPIAMConfigReqDefaults(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/integrations/awsoidc/listdatabases_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestListDatabasesIAMConfigReqDefaults(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/tbot/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (

"github.com/gravitational/teleport/lib/tbot/bot"
"github.com/gravitational/teleport/lib/tbot/botfs"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestConfigFile(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion lib/tbot/service_identity_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import (
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/ssh"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

const (
Expand Down
2 changes: 1 addition & 1 deletion lib/tbot/service_kubernetes_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/identity"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

// Fairly ugly hardcoded certs to use in the generation so that the tests are
Expand Down
2 changes: 1 addition & 1 deletion lib/tbot/service_spiffe_workload_api_sds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import (
"github.com/gravitational/teleport/lib/tbot/spiffe"
"github.com/gravitational/teleport/lib/tbot/spiffe/workloadattest"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
"github.com/gravitational/teleport/tool/teleport/testenv"
)

Expand Down
2 changes: 1 addition & 1 deletion lib/tpm/tpm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/stretchr/testify/assert"

"github.com/gravitational/teleport/lib/tpm"
"github.com/gravitational/teleport/lib/utils/golden"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)

func TestPrintQuery(t *testing.T) {
Expand Down
File renamed without changes.
239 changes: 239 additions & 0 deletions lib/utils/testutils/testutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package testutils

import (
"fmt"
"reflect"
"strings"
)

// ExhaustiveNonEmpty is a helper that uses reflection to check if a given value and its sub-elements are non-empty. Exhaustive
// non-emptiness is evaluated in the following ways:
//
// - Pointers/Interfaces are considered exhaustively non-empty if their underlying value is exhaustively non-empty.
// - Slices/Arrays are considered exhaustively non-empty if they have at least one exhaustively non-empty element.
// - Maps are considered exhaustively non-empty if they have at least one exhaustively non-empty value.
// - Structs are considered exhaustively non-empty if all their exported fields are non-empty.
// - All other types are considered exhaustively non-empty if reflect.Value.IsZero is false.
//
// The ignoreOpts parameter is a variadic list of strings that represent the fully qualified field names of struct fields that
// should be ignored when checking for non-emptiness. For example, to ignore the field Bar on type Foo pass in "Foo.Bar" as an
// ignore option. Note that embedded type fields have to be ignored by the parent type's name (i.e. `Outer.Field` rather than
// `Inner.Field`).
//
// The intended usecase of this helper is to ensure that new fields added to a struct are included in test cases that want to
// cover all fields. For example, a test of serialization/deserialization logic might assert that the sample struct is exhaustively
// non-empty in order to force new fields to be covered by the test.
func ExhaustiveNonEmpty(item any, ignoreOpts ...string) bool {
value := reflect.ValueOf(item)

ignore := make(map[string]struct{}, len(ignoreOpts))
for _, opt := range ignoreOpts {
ignore[opt] = struct{}{}
}

return exhaustiveNonEmpty(value, ignore)
}

func exhaustiveNonEmpty(value reflect.Value, ignore map[string]struct{}) bool {
if !value.IsValid() {
// indicates that reflect.ValueOf/Value.Elem was called on a nil pointer/interface
return false
}

switch value.Kind() {
case reflect.Pointer, reflect.Interface:
// recursively check the underlying value
return exhaustiveNonEmpty(value.Elem(), ignore)
case reflect.Slice, reflect.Array:
if value.Len() == 0 {
return false
}

for i := 0; i < value.Len(); i++ {
if exhaustiveNonEmpty(value.Index(i), ignore) {
return true
}
}
return false
case reflect.Map:
if value.Len() == 0 {
return false
}

mr := value.MapRange()

for mr.Next() {
if exhaustiveNonEmpty(mr.Value(), ignore) {
return true
}
}

return false
case reflect.Struct:
var fieldsConsidered int
for _, vf := range reflect.VisibleFields(value.Type()) {
if vf.Anonymous {
// skip the embedded type itself since this loop will
// end up processing each of the embedded type's fields as
// a member of this type's fields.
continue
}

if !vf.IsExported() {
// skip non-exported fields
continue
}

fieldsConsidered++

// skip fields if `<type>.<field>` is in the ignore list
if _, ok := ignore[fmt.Sprintf("%s.%s", value.Type().Name(), vf.Name)]; ok {
continue
}

if !exhaustiveNonEmpty(value.FieldByIndex(vf.Index), ignore) {
return false
}
}

if fieldsConsidered == 0 {
// fallback to basic nonzeroness check for structs with no exported fields (necessary
// in order to achieve expected behavior for types like time.Time).
return !value.IsZero()
}

return true
default:
// fallback to basic nonzeroness check for all other types
return !value.IsZero()
}
}

// FindAllEmpty is a helper that uses reflection to find all empty sub-components of a given value. It functions similarly to the ExhaustiveNonEmpty
// check, but may return a non-empty list of paths in cases where ExhaustiveNonEmpty would return false since it records all empty members of
// collections even if the collection contains a non-empty member.
//
// The intended usecase for FindAllEmpty is to build helpful failure messages in tests that assert that a struct is non-empty.
//
// Note that this function panics if the top-level item passed in is nil.
func FindAllEmpty(item any, ignoreOpts ...string) []string {
value := reflect.ValueOf(item)

if !value.IsValid() {
panic("FindAllEmpty called with nil top-level item")
}

// dereference pointers and interfaces so that the root find logic starts from
// a concrete type (makes the returned paths more consistent/understandable).
switch value.Kind() {
case reflect.Ptr, reflect.Interface:
if value.IsNil() {
panic("FindAllEmpty called with nil top-level pointer/interface")
}
return FindAllEmpty(value.Elem().Interface(), ignoreOpts...)
}

ignore := make(map[string]struct{}, len(ignoreOpts))
for _, opt := range ignoreOpts {
ignore[opt] = struct{}{}
}

path := []string{value.Type().Name()}

return findAllEmpty(value, ignore, path)
}

func findAllEmpty(value reflect.Value, ignore map[string]struct{}, path []string) []string {
if !value.IsValid() {
// indicates that reflect.ValueOf/Value.Elem was called on a nil pointer/interface
return []string{strings.Join(path, ".")}
}

switch value.Kind() {
case reflect.Pointer, reflect.Interface:
// recursively check the underlying value
return findAllEmpty(value.Elem(), ignore, path)
case reflect.Slice, reflect.Array:
if value.Len() == 0 {
return []string{strings.Join(path, ".")}
}

var emptyPaths []string
for i := 0; i < value.Len(); i++ {
emptyPaths = append(emptyPaths, findAllEmpty(value.Index(i), ignore, append(path, fmt.Sprintf("%d", i)))...)
}
return emptyPaths
case reflect.Map:
if value.Len() == 0 {
return []string{strings.Join(path, ".")}
}

mr := value.MapRange()

var emptyPaths []string
for mr.Next() {
emptyPaths = append(emptyPaths, findAllEmpty(mr.Value(), ignore, append(path, fmt.Sprintf("%v", mr.Key().Interface())))...)
}

return emptyPaths
case reflect.Struct:
emptyPaths := make([]string, 0, value.NumField())
var fieldsConsidered int
for _, vf := range reflect.VisibleFields(value.Type()) {
if vf.Anonymous {
// skip the embedded type itself since this loop will
// end up processing each of the embedded type's fields as
// a member of this type's fields.
continue
}

if !vf.IsExported() {
// skip non-exported fields
continue
}

fieldsConsidered++

// skip fields if `<type>.<field>` is in the ignore list
if _, ok := ignore[fmt.Sprintf("%s.%s", value.Type().Name(), vf.Name)]; ok {
continue
}

emptyPaths = append(emptyPaths, findAllEmpty(value.FieldByIndex(vf.Index), ignore, append(path, vf.Name))...)
}

if fieldsConsidered == 0 {
// fallback to basic nonzeroness check for structs with no exported fields (necessary
// in order to achieve expected behavior for types like time.Time).
if value.IsZero() {
return []string{strings.Join(path, ".")}
}
}

return emptyPaths
default:
// fallback to basic nonzeroness check for all other types
if value.IsZero() {
return []string{strings.Join(path, ".")}
}
return nil
}
}
Loading
Loading