-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* add emtiness testutils * move golden to testutils
- Loading branch information
1 parent
a174bd4
commit b39e3f7
Showing
12 changed files
with
812 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.