From 8d0038d8de5822c23c10b6eb67a97efb9f4f1af4 Mon Sep 17 00:00:00 2001 From: Forrest <30576607+fspmarshall@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:00:55 -0800 Subject: [PATCH] add emptiness testutils (#50655) (#50878) * add emtiness testutils * move golden to testutils --- lib/autoupdate/agent/updater_test.go | 2 +- lib/client/db/postgres/repl/repl_test.go | 2 +- lib/config/openssh/openssh_test.go | 2 +- lib/config/systemd_test.go | 2 +- .../awsoidc/access_graph_aws_sync_test.go | 2 +- .../awsoidc/aws_app_access_iam_config_test.go | 2 +- .../awsoidc/deployservice_iam_config_test.go | 2 +- .../awsoidc/ec2_ssm_iam_config_test.go | 2 +- .../awsoidc/eice_iam_config_test.go | 2 +- .../awsoidc/eks_iam_config_test.go | 2 +- .../awsoidc/idp_iam_config_test.go | 2 +- .../awsoidc/listdatabases_iam_config_test.go | 2 +- lib/tbot/config/config_test.go | 2 +- lib/tbot/service_identity_output_test.go | 2 +- lib/tbot/service_kubernetes_output_test.go | 2 +- .../service_spiffe_workload_api_sds_test.go | 2 +- lib/tpm/tpm_test.go | 2 +- lib/utils/{ => testutils}/golden/golden.go | 0 lib/utils/testutils/testutils.go | 239 ++++++++ lib/utils/testutils/testutils_test.go | 564 ++++++++++++++++++ tool/tbot/main_test.go | 2 +- tool/tbot/systemd_test.go | 2 +- tool/tctl/common/workload_identity_test.go | 2 +- 23 files changed, 823 insertions(+), 20 deletions(-) rename lib/utils/{ => testutils}/golden/golden.go (100%) create mode 100644 lib/utils/testutils/testutils.go create mode 100644 lib/utils/testutils/testutils_test.go diff --git a/lib/autoupdate/agent/updater_test.go b/lib/autoupdate/agent/updater_test.go index 6568fbaede9e2..5ec93b43be9cc 100644 --- a/lib/autoupdate/agent/updater_test.go +++ b/lib/autoupdate/agent/updater_test.go @@ -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) { diff --git a/lib/client/db/postgres/repl/repl_test.go b/lib/client/db/postgres/repl/repl_test.go index e2d780f9ab9c7..58edeb7947443 100644 --- a/lib/client/db/postgres/repl/repl_test.go +++ b/lib/client/db/postgres/repl/repl_test.go @@ -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) { diff --git a/lib/config/openssh/openssh_test.go b/lib/config/openssh/openssh_test.go index 73eca420849d3..c040070adfa85 100644 --- a/lib/config/openssh/openssh_test.go +++ b/lib/config/openssh/openssh_test.go @@ -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) { diff --git a/lib/config/systemd_test.go b/lib/config/systemd_test.go index 193581512ad60..8283a861147f1 100644 --- a/lib/config/systemd_test.go +++ b/lib/config/systemd_test.go @@ -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) { diff --git a/lib/integrations/awsoidc/access_graph_aws_sync_test.go b/lib/integrations/awsoidc/access_graph_aws_sync_test.go index 8902c4b63df29..080d1c0475f3d 100644 --- a/lib/integrations/awsoidc/access_graph_aws_sync_test.go +++ b/lib/integrations/awsoidc/access_graph_aws_sync_test.go @@ -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) { diff --git a/lib/integrations/awsoidc/aws_app_access_iam_config_test.go b/lib/integrations/awsoidc/aws_app_access_iam_config_test.go index 9933b0d86150b..b2140be2d8bff 100644 --- a/lib/integrations/awsoidc/aws_app_access_iam_config_test.go +++ b/lib/integrations/awsoidc/aws_app_access_iam_config_test.go @@ -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) { diff --git a/lib/integrations/awsoidc/deployservice_iam_config_test.go b/lib/integrations/awsoidc/deployservice_iam_config_test.go index 21ce13bb1873a..48d33af097f0c 100644 --- a/lib/integrations/awsoidc/deployservice_iam_config_test.go +++ b/lib/integrations/awsoidc/deployservice_iam_config_test.go @@ -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{}) { diff --git a/lib/integrations/awsoidc/ec2_ssm_iam_config_test.go b/lib/integrations/awsoidc/ec2_ssm_iam_config_test.go index 7b08c4e45dbec..2dd1612992a48 100644 --- a/lib/integrations/awsoidc/ec2_ssm_iam_config_test.go +++ b/lib/integrations/awsoidc/ec2_ssm_iam_config_test.go @@ -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) { diff --git a/lib/integrations/awsoidc/eice_iam_config_test.go b/lib/integrations/awsoidc/eice_iam_config_test.go index 71efcf6dc03fc..db7bc840f7e5d 100644 --- a/lib/integrations/awsoidc/eice_iam_config_test.go +++ b/lib/integrations/awsoidc/eice_iam_config_test.go @@ -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) { diff --git a/lib/integrations/awsoidc/eks_iam_config_test.go b/lib/integrations/awsoidc/eks_iam_config_test.go index c0e6a7022eb50..83b2063ef4eef 100644 --- a/lib/integrations/awsoidc/eks_iam_config_test.go +++ b/lib/integrations/awsoidc/eks_iam_config_test.go @@ -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) { diff --git a/lib/integrations/awsoidc/idp_iam_config_test.go b/lib/integrations/awsoidc/idp_iam_config_test.go index 7961fc8255232..50057b2bff77f 100644 --- a/lib/integrations/awsoidc/idp_iam_config_test.go +++ b/lib/integrations/awsoidc/idp_iam_config_test.go @@ -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) { diff --git a/lib/integrations/awsoidc/listdatabases_iam_config_test.go b/lib/integrations/awsoidc/listdatabases_iam_config_test.go index 08585451fa8a8..602584b43f08e 100644 --- a/lib/integrations/awsoidc/listdatabases_iam_config_test.go +++ b/lib/integrations/awsoidc/listdatabases_iam_config_test.go @@ -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) { diff --git a/lib/tbot/config/config_test.go b/lib/tbot/config/config_test.go index 9769b89253005..20d8449a997d4 100644 --- a/lib/tbot/config/config_test.go +++ b/lib/tbot/config/config_test.go @@ -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) { diff --git a/lib/tbot/service_identity_output_test.go b/lib/tbot/service_identity_output_test.go index 00b490ea6caad..cf49af30b73ac 100644 --- a/lib/tbot/service_identity_output_test.go +++ b/lib/tbot/service_identity_output_test.go @@ -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 ( diff --git a/lib/tbot/service_kubernetes_output_test.go b/lib/tbot/service_kubernetes_output_test.go index 4d490359811e4..36ce0b15327b8 100644 --- a/lib/tbot/service_kubernetes_output_test.go +++ b/lib/tbot/service_kubernetes_output_test.go @@ -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 diff --git a/lib/tbot/service_spiffe_workload_api_sds_test.go b/lib/tbot/service_spiffe_workload_api_sds_test.go index bbe6d2af32edb..7c59b5e09b949 100644 --- a/lib/tbot/service_spiffe_workload_api_sds_test.go +++ b/lib/tbot/service_spiffe_workload_api_sds_test.go @@ -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" ) diff --git a/lib/tpm/tpm_test.go b/lib/tpm/tpm_test.go index e95ea18612518..8ff04b701409d 100644 --- a/lib/tpm/tpm_test.go +++ b/lib/tpm/tpm_test.go @@ -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) { diff --git a/lib/utils/golden/golden.go b/lib/utils/testutils/golden/golden.go similarity index 100% rename from lib/utils/golden/golden.go rename to lib/utils/testutils/golden/golden.go diff --git a/lib/utils/testutils/testutils.go b/lib/utils/testutils/testutils.go new file mode 100644 index 0000000000000..2bbe39dd58f53 --- /dev/null +++ b/lib/utils/testutils/testutils.go @@ -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 . + */ + +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 `.` 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 `.` 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 + } +} diff --git a/lib/utils/testutils/testutils_test.go b/lib/utils/testutils/testutils_test.go new file mode 100644 index 0000000000000..35c73d15b7122 --- /dev/null +++ b/lib/utils/testutils/testutils_test.go @@ -0,0 +1,564 @@ +/* + * 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 . + */ + +package testutils + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestExhaustiveNonEmptyBasics tests the basic functionality of ExhaustiveNonEmpty using various +// combinations of simple types. +func TestExhaustiveNonEmptyBasics(t *testing.T) { + t.Parallel() + tts := []struct { + desc string + value any + expect bool + }{ + { + desc: "basic nil", + value: nil, + expect: false, + }, + { + desc: "nil slice", + value: []string(nil), + expect: false, + }, + { + desc: "empty slice", + value: []string{}, + expect: false, + }, + { + desc: "slice with empty element", + value: []string{""}, + expect: false, + }, + { + desc: "non-empty slice", + value: []string{"a"}, + expect: true, + }, + { + desc: "slice with mix of empty and non-empty elements", + value: []string{"", "a"}, + expect: true, + }, + { + desc: "nil pointer", + value: (*string)(nil), + expect: false, + }, + { + desc: "pointer to empty string", + value: new(string), + expect: false, + }, + { + desc: "pointer to non-empty string", + value: func() *string { + s := "a" + return &s + }(), + expect: true, + }, + { + desc: "zero int", + value: int(0), + expect: false, + }, + { + desc: "non-zero int", + value: int(1), + expect: true, + }, + { + desc: "nil map", + value: map[string]string(nil), + expect: false, + }, + { + desc: "empty map", + value: map[string]string{}, + expect: false, + }, + { + desc: "map with empty value", + value: map[string]string{ + "a": "", + }, + expect: false, + }, + { + desc: "map with non-empty value", + value: map[string]string{ + "a": "b", + }, + expect: true, + }, + { + desc: "map with mix of empty and non-empty values", + value: map[string]string{ + "a": "", + "b": "c", + }, + expect: true, + }, + { + desc: "zero time", + value: time.Time{}, + expect: false, + }, + { + desc: "non-zero time", + value: time.Now(), + expect: true, + }, + } + + for _, tt := range tts { + t.Run(tt.desc, func(t *testing.T) { + require.Equal(t, tt.expect, ExhaustiveNonEmpty(tt.value), "value=%+v", tt.value) + }) + } +} + +// TestExhaustiveNonEmptyStruct tests the basic functionality of ExhaustiveNonEmpty using different struct field/nesting +// scenarios. This test also covers the behavior of struct field ignore options. +func TestExhaustiveNonEmptyStruct(t *testing.T) { + t.Parallel() + type Inner struct { + Field string + } + + type Outer struct { + Inner + Slice []Inner + Pointer *Inner + Value Inner + Map map[string]Inner + } + + newNonEmpty := func() Outer { + return Outer{ + Inner: Inner{ + Field: "a", + }, + Slice: []Inner{ + {Field: "b"}, + }, + Pointer: &Inner{Field: "c"}, + Value: Inner{Field: "d"}, + Map: map[string]Inner{ + "e": {Field: "f"}, + }, + } + } + + tts := []struct { + desc string + value any + ignore []string + expect bool + }{ + { + desc: "empty struct", + value: Outer{}, + expect: false, + }, + { + desc: "non-empty struct", + value: newNonEmpty(), + expect: true, + }, + { + desc: "pointer to empty struct", + value: new(Outer), + expect: false, + }, + { + desc: "pointer to non-empty struct", + value: func() *Outer { + v := newNonEmpty() + return &v + }(), + expect: true, + }, + { + desc: "struct with empty embed", + value: func() Outer { + v := newNonEmpty() + v.Inner = Inner{} + return v + }(), + expect: false, + }, + { + desc: "struct with nil slice", + value: func() Outer { + v := newNonEmpty() + v.Slice = nil + return v + }(), + expect: false, + }, + { + desc: "struct with empty slice", + value: func() Outer { + v := newNonEmpty() + v.Slice = []Inner{} + return v + }(), + expect: false, + }, + { + desc: "struct with empty slice element", + value: func() Outer { + v := newNonEmpty() + v.Slice = []Inner{{}} + return v + }(), + expect: false, + }, + { + desc: "struct with nil pointer", + value: func() Outer { + v := newNonEmpty() + v.Pointer = nil + return v + }(), + expect: false, + }, + { + desc: "struct with empty pointer", + value: func() Outer { + v := newNonEmpty() + v.Pointer = &Inner{} + return v + }(), + expect: false, + }, + { + desc: "struct with empty value", + value: func() Outer { + v := newNonEmpty() + v.Value = Inner{} + return v + }(), + expect: false, + }, + { + desc: "struct with nil map", + value: func() Outer { + v := newNonEmpty() + v.Map = nil + return v + }(), + expect: false, + }, + { + desc: "struct with empty map", + value: func() Outer { + v := newNonEmpty() + v.Map = map[string]Inner{} + return v + }(), + expect: false, + }, + { + desc: "struct with empty map value", + value: func() Outer { + v := newNonEmpty() + v.Map = map[string]Inner{"a": {}} + return v + }(), + expect: false, + }, + { + desc: "ignore top-level field", + value: func() Outer { + v := newNonEmpty() + v.Value = Inner{} + return v + }(), + ignore: []string{"Outer.Value"}, + expect: true, + }, + { + desc: "ignore embedded field", + value: func() Outer { + v := newNonEmpty() + v.Inner = Inner{} + return v + }(), + ignore: []string{"Outer.Field"}, // embedded ignores use the outer type name + expect: true, + }, + { + desc: "ignore slice element field", + value: func() Outer { + v := newNonEmpty() + v.Slice = []Inner{{}} + return v + }(), + ignore: []string{"Inner.Field"}, + expect: true, + }, + { + desc: "ignore pointer field", + value: func() Outer { + v := newNonEmpty() + v.Pointer = &Inner{} + return v + }(), + ignore: []string{"Inner.Field"}, + expect: true, + }, + { + desc: "ignore map value field", + value: func() Outer { + v := newNonEmpty() + v.Map = map[string]Inner{"a": {}} + return v + }(), + ignore: []string{"Inner.Field"}, + expect: true, + }, + } + + for _, tt := range tts { + t.Run(tt.desc, func(t *testing.T) { + require.Equal(t, tt.expect, ExhaustiveNonEmpty(tt.value, tt.ignore...), "value=%+v", tt.value) + }) + } +} + +// TestFindAllEmptyStruct tests the basic functionality of FindAllEmpty using different struct field/nesting +// scenarios. This test also covers the behavior of struct field ignore options. +func TestFindAllEmptyStruct(t *testing.T) { + t.Parallel() + type Inner struct { + Field string + } + + type Outer struct { + Inner + Slice []Inner + Pointer *Inner + Value Inner + Map map[string]Inner + } + + newNonEmpty := func() Outer { + return Outer{ + Inner: Inner{ + Field: "a", + }, + Slice: []Inner{ + {Field: "b"}, + }, + Pointer: &Inner{Field: "c"}, + Value: Inner{Field: "d"}, + Map: map[string]Inner{ + "e": {Field: "f"}, + }, + } + } + + tts := []struct { + desc string + value any + ignore []string + expect []string + }{ + { + desc: "empty struct", + value: Outer{}, + expect: []string{"Outer.Field", "Outer.Slice", "Outer.Pointer", "Outer.Value.Field", "Outer.Map"}, + }, + { + desc: "non-empty struct", + value: newNonEmpty(), + expect: nil, + }, + { + desc: "pointer to empty struct", + value: new(Outer), + expect: []string{"Outer.Field", "Outer.Slice", "Outer.Pointer", "Outer.Value.Field", "Outer.Map"}, + }, + { + desc: "pointer to non-empty struct", + value: func() *Outer { + v := newNonEmpty() + return &v + }(), + expect: nil, + }, + { + desc: "struct with empty embed", + value: func() Outer { + v := newNonEmpty() + v.Inner = Inner{} + return v + }(), + expect: []string{"Outer.Field"}, + }, + { + desc: "struct with nil slice", + value: func() Outer { + v := newNonEmpty() + v.Slice = nil + return v + }(), + expect: []string{"Outer.Slice"}, + }, + { + desc: "struct with empty slice", + value: func() Outer { + v := newNonEmpty() + v.Slice = []Inner{} + return v + }(), + expect: []string{"Outer.Slice"}, + }, + { + desc: "struct with empty slice element", + value: func() Outer { + v := newNonEmpty() + v.Slice = []Inner{{}} + return v + }(), + expect: []string{"Outer.Slice.0.Field"}, + }, + { + desc: "struct with nil pointer", + value: func() Outer { + v := newNonEmpty() + v.Pointer = nil + return v + }(), + expect: []string{"Outer.Pointer"}, + }, + { + desc: "struct with empty pointer", + value: func() Outer { + v := newNonEmpty() + v.Pointer = &Inner{} + return v + }(), + expect: []string{"Outer.Pointer.Field"}, + }, + { + desc: "struct with empty value", + value: func() Outer { + v := newNonEmpty() + v.Value = Inner{} + return v + }(), + expect: []string{"Outer.Value.Field"}, + }, + { + desc: "struct with nil map", + value: func() Outer { + v := newNonEmpty() + v.Map = nil + return v + }(), + expect: []string{"Outer.Map"}, + }, + { + desc: "struct with empty map", + value: func() Outer { + v := newNonEmpty() + v.Map = map[string]Inner{} + return v + }(), + expect: []string{"Outer.Map"}, + }, + { + desc: "struct with empty map value", + value: func() Outer { + v := newNonEmpty() + v.Map = map[string]Inner{"a": {}} + return v + }(), + expect: []string{"Outer.Map.a.Field"}, + }, + { + desc: "ignore top-level field", + value: func() Outer { + v := newNonEmpty() + v.Value = Inner{} + return v + }(), + ignore: []string{"Outer.Value"}, + expect: nil, + }, + { + desc: "ignore embedded field", + value: func() Outer { + v := newNonEmpty() + v.Inner = Inner{} + return v + }(), + ignore: []string{"Outer.Field"}, // embedded ignores use the outer type name + expect: nil, + }, + { + desc: "ignore slice element field", + value: func() Outer { + v := newNonEmpty() + v.Slice = []Inner{{}} + return v + }(), + ignore: []string{"Inner.Field"}, + expect: nil, + }, + { + desc: "ignore pointer field", + value: func() Outer { + v := newNonEmpty() + v.Pointer = &Inner{} + return v + }(), + ignore: []string{"Inner.Field"}, + expect: nil, + }, + { + desc: "ignore map value field", + value: func() Outer { + v := newNonEmpty() + v.Map = map[string]Inner{"a": {}} + return v + }(), + ignore: []string{"Inner.Field"}, + expect: nil, + }, + } + + for _, tt := range tts { + t.Run(tt.desc, func(t *testing.T) { + require.ElementsMatch(t, tt.expect, FindAllEmpty(tt.value, tt.ignore...), "value=%+v", tt.value) + }) + } +} diff --git a/tool/tbot/main_test.go b/tool/tbot/main_test.go index d45118d95c298..2d00c6ec7c804 100644 --- a/tool/tbot/main_test.go +++ b/tool/tbot/main_test.go @@ -26,7 +26,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/gravitational/teleport/lib/utils/golden" + "github.com/gravitational/teleport/lib/utils/testutils/golden" ) func TestRun_Configure(t *testing.T) { diff --git a/tool/tbot/systemd_test.go b/tool/tbot/systemd_test.go index 10705aeaa44df..248a696636a8f 100644 --- a/tool/tbot/systemd_test.go +++ b/tool/tbot/systemd_test.go @@ -31,7 +31,7 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/lib/utils" - "github.com/gravitational/teleport/lib/utils/golden" + "github.com/gravitational/teleport/lib/utils/testutils/golden" ) func TestInstallSystemdCmd(t *testing.T) { diff --git a/tool/tctl/common/workload_identity_test.go b/tool/tctl/common/workload_identity_test.go index 818ec2cb1d8f4..92c8c9006ba24 100644 --- a/tool/tctl/common/workload_identity_test.go +++ b/tool/tctl/common/workload_identity_test.go @@ -34,7 +34,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" "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" )