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"
)