Skip to content

Commit 4ee7ed0

Browse files
Maximeonsi
authored andcommitted
gstruct handles extra unexported fields
1 parent 529d408 commit 4ee7ed0

File tree

4 files changed

+130
-6
lines changed

4 files changed

+130
-6
lines changed

docs/index.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2786,7 +2786,14 @@ Due to the global nature of these methods, keep in mind that signaling processes
27862786

27872787
### Testing type `struct`
27882788

2789-
`gstruct` provides the `FieldsMatcher` through the `MatchAllFields` and `MatchFields` functions for applying a separate matcher to each field of a struct:
2789+
`gstruct` provides the `FieldsMatcher` through the `MatchAllFields` and `MatchFields` functions for applying a separate matcher to each field of a struct.
2790+
2791+
To match a subset or superset of a struct, you should use the `MatchFields` function with the `IgnoreExtras`, `IgnoreUnexportedExtras` and `IgnoreMissing` options.
2792+
The options can be combined with the binary or, for instance : `IgnoreMissing|IgnoreExtras`.
2793+
2794+
#### Match all fields
2795+
2796+
`MatchAllFields` requires that every field is matched, and each matcher is mapped to a field. This is useful for test maintainability, as it ensures that all fields are tested, and will fail in the future if you add or remove a field and forget to update the test, e.g.
27902797

27912798
```go
27922799
actual := struct{
@@ -2801,7 +2808,9 @@ Expect(actual).To(MatchAllFields(Fields{
28012808
}))
28022809
```
28032810

2804-
`MatchAllFields` requires that every field is matched, and each matcher is mapped to a field. To match a subset or superset of a struct, you should use the `MatchFields` function with the `IgnoreExtras` and `IgnoreMissing` options. `IgnoreExtras` will ignore fields that don't map to a matcher, e.g.
2811+
#### Ignore extra fields
2812+
2813+
`IgnoreExtras` will ignore fields that don't map to a matcher, e.g.
28052814

28062815
```go
28072816
Expect(actual).To(MatchFields(IgnoreExtras, Fields{
@@ -2811,6 +2820,28 @@ Expect(actual).To(MatchFields(IgnoreExtras, Fields{
28112820
}))
28122821
```
28132822

2823+
Using IgnoreExtras will ignore any new field that you will add to the struct in the future, you might want to consider using `gstruct.Ignore()` instead if you want to ignore only specific fields.
2824+
2825+
#### Ignore unexported extra fields
2826+
2827+
`IgnoreUnexportedExtras` will ignore fields that don't map to a matcher, but only if they are unexported e.g.
2828+
2829+
```go
2830+
Expect(actual).To(MatchFields(IgnoreUnexportedExtras, Fields{
2831+
"A": BeNumerically("<", 10),
2832+
"B": BeTrue(),
2833+
// Ignore lack of "c" in the matcher.
2834+
// But does not ignore "C" in the matcher, because it is exported.
2835+
}))
2836+
```
2837+
2838+
This is useful because gstruct uses the `reflect` package to access the fields of a struct, and it will not be able to access unexported fields.
2839+
This is a compromise between using MatchAllFields and MatchFields with IgnoreExtras, as it allows you to ignore unexported fields without having to specify them in the matcher.
2840+
If you prefer to list the unexported fields you want to ignore, you can use `gstruct.Ignore()` instead, the matcher will make sure to not use reflect on those fields.
2841+
2842+
2843+
#### Ignore missing fields
2844+
28142845
`IgnoreMissing` will ignore matchers that don't map to a field, e.g.
28152846

28162847
```go
@@ -2822,8 +2853,6 @@ Expect(actual).To(MatchFields(IgnoreMissing, Fields{
28222853
}))
28232854
```
28242855

2825-
The options can be combined with the binary or: `IgnoreMissing|IgnoreExtras`.
2826-
28272856
### Testing type slice
28282857

28292858
`gstruct` provides the `ElementsMatcher` through the `MatchAllElements` and `MatchElements` function for applying a separate matcher to each element, identified by an `Identifier` function:

gstruct/fields.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"reflect"
99
"runtime/debug"
1010
"strings"
11+
"unicode"
1112

1213
"github.com/onsi/gomega/format"
1314
errorsutil "github.com/onsi/gomega/gstruct/errors"
@@ -65,6 +66,7 @@ func MatchFields(options Options, fields Fields) types.GomegaMatcher {
6566
return &FieldsMatcher{
6667
Fields: fields,
6768
IgnoreExtras: options&IgnoreExtras != 0,
69+
IgnoreUnexportedExtras: options&IgnoreUnexportedExtras != 0,
6870
IgnoreMissing: options&IgnoreMissing != 0,
6971
}
7072
}
@@ -75,6 +77,8 @@ type FieldsMatcher struct {
7577

7678
// Whether to ignore extra elements or consider it an error.
7779
IgnoreExtras bool
80+
// Whether to ignore unexported extra elements or consider it an error.
81+
IgnoreUnexportedExtras bool
7882
// Whether to ignore missing elements or consider it an error.
7983
IgnoreMissing bool
8084

@@ -97,6 +101,14 @@ func (m *FieldsMatcher) Match(actual any) (success bool, err error) {
97101
return true, nil
98102
}
99103

104+
func isExported(fieldName string) bool {
105+
if fieldName == "" {
106+
return false
107+
}
108+
r := []rune(fieldName)[0]
109+
return unicode.IsUpper(r)
110+
}
111+
100112
func (m *FieldsMatcher) matchFields(actual any) (errs []error) {
101113
val := reflect.ValueOf(actual)
102114
typ := val.Type()
@@ -116,13 +128,21 @@ func (m *FieldsMatcher) matchFields(actual any) (errs []error) {
116128

117129
matcher, expected := m.Fields[fieldName]
118130
if !expected {
131+
if m.IgnoreUnexportedExtras && !isExported(fieldName) {
132+
return nil
133+
}
119134
if !m.IgnoreExtras {
120135
return fmt.Errorf("unexpected field %s: %+v", fieldName, actual)
121136
}
122137
return nil
123138
}
124139

125-
field := val.Field(i).Interface()
140+
var field any
141+
if _, isIgnoreMatcher := matcher.(*IgnoreMatcher) ; isIgnoreMatcher {
142+
field = struct {}{} // the matcher does not care about the actual value
143+
} else {
144+
field = val.Field(i).Interface()
145+
}
126146

127147
match, err := matcher.Match(field)
128148
if err != nil {

gstruct/fields_test.go

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import (
99
var _ = Describe("Struct", func() {
1010
allFields := struct{ A, B string }{"a", "b"}
1111
missingFields := struct{ A string }{"a"}
12-
extraFields := struct{ A, B, C string }{"a", "b", "c"}
12+
extraFields := struct{ A, B, C, D string }{"a", "b", "c", "d"}
13+
extraUnexportedFields := struct{ A, B, c, d string }{"a", "b", "c", "d"}
1314
emptyFields := struct{ A, B string }{}
1415

1516
It("should strictly match all fields", func() {
@@ -20,6 +21,7 @@ var _ = Describe("Struct", func() {
2021
Expect(allFields).Should(m, "should match all fields")
2122
Expect(missingFields).ShouldNot(m, "should fail with missing fields")
2223
Expect(extraFields).ShouldNot(m, "should fail with extra fields")
24+
Expect(extraUnexportedFields).ShouldNot(m, "should fail with extra unexported fields")
2325
Expect(emptyFields).ShouldNot(m, "should fail with empty fields")
2426

2527
m = MatchAllFields(Fields{
@@ -43,6 +45,7 @@ var _ = Describe("Struct", func() {
4345
Expect(allFields).Should(m, "should match all fields")
4446
Expect(missingFields).Should(m, "should ignore missing fields")
4547
Expect(extraFields).ShouldNot(m, "should fail with extra fields")
48+
Expect(extraUnexportedFields).ShouldNot(m, "should fail extra unexported fields")
4649
Expect(emptyFields).ShouldNot(m, "should fail with empty fields")
4750
})
4851

@@ -54,9 +57,76 @@ var _ = Describe("Struct", func() {
5457
Expect(allFields).Should(m, "should match all fields")
5558
Expect(missingFields).ShouldNot(m, "should fail with missing fields")
5659
Expect(extraFields).Should(m, "should ignore extra fields")
60+
Expect(extraUnexportedFields).Should(m, "should ignore unexported extra fields")
5761
Expect(emptyFields).ShouldNot(m, "should fail with empty fields")
5862
})
5963

64+
It("should ignore unexported extra fields", func() {
65+
m := MatchFields(IgnoreUnexportedExtras, Fields{
66+
"B": Equal("b"),
67+
"A": Equal("a"),
68+
})
69+
Expect(allFields).Should(m, "should match all fields")
70+
Expect(missingFields).ShouldNot(m, "should fail with missing fields")
71+
Expect(extraFields).ShouldNot(m, "should fail with exported extra fields")
72+
Expect(extraUnexportedFields).Should(m, "should ignore unexported extra fields")
73+
Expect(emptyFields).ShouldNot(m, "should fail with empty fields")
74+
})
75+
76+
It("should ignore ignored fields", func() {
77+
m := MatchAllFields(Fields{
78+
"B": Equal("b"),
79+
"A": Equal("a"),
80+
})
81+
Expect(extraFields).ShouldNot(m, "should fail with exported extra fields")
82+
83+
m = MatchAllFields(Fields{
84+
"B": Equal("b"),
85+
"A": Equal("a"),
86+
"C": Ignore(),
87+
})
88+
Expect(extraFields).ShouldNot(m, "should fail with exported extra fields partially ignored")
89+
90+
m = MatchAllFields(Fields{
91+
"B": Equal("b"),
92+
"A": Equal("a"),
93+
"C": Ignore(),
94+
"D": Ignore(),
95+
})
96+
Expect(extraFields).Should(m, "should match with all remaining fields ignored")
97+
})
98+
99+
It("should ignore ignored unexported fields", func() {
100+
m := MatchAllFields(Fields{
101+
"B": Equal("b"),
102+
"A": Equal("a"),
103+
})
104+
Expect(extraUnexportedFields).ShouldNot(m, "should fail with exported extra fields")
105+
106+
m = MatchAllFields(Fields{
107+
"B": Equal("b"),
108+
"A": Equal("a"),
109+
"c": Ignore(),
110+
})
111+
Expect(extraUnexportedFields).ShouldNot(m, "should fail with exported extra fields partially ignored")
112+
113+
m = MatchAllFields(Fields{
114+
"B": Equal("b"),
115+
"A": Equal("a"),
116+
"c": Ignore(),
117+
"d": Ignore(),
118+
})
119+
Expect(extraUnexportedFields).Should(m, "should match with all remaining fields ignored")
120+
121+
m = MatchAllFields(Fields{
122+
"B": Equal("b"),
123+
"A": Equal("a"),
124+
"c": Ignore(),
125+
"d": Reject(),
126+
})
127+
Expect(extraUnexportedFields).ShouldNot(m, "should fail if we used Reject() on an unexported field")
128+
})
129+
60130
It("should ignore missing and extra fields", func() {
61131
m := MatchFields(IgnoreMissing|IgnoreExtras, Fields{
62132
"B": Equal("b"),
@@ -65,6 +135,7 @@ var _ = Describe("Struct", func() {
65135
Expect(allFields).Should(m, "should match all fields")
66136
Expect(missingFields).Should(m, "should ignore missing fields")
67137
Expect(extraFields).Should(m, "should ignore extra fields")
138+
Expect(extraUnexportedFields).Should(m, "should ignore unexported extra fields")
68139
Expect(emptyFields).ShouldNot(m, "should fail with empty fields")
69140

70141
m = MatchFields(IgnoreMissing|IgnoreExtras, Fields{

gstruct/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ const (
1212
//considered by the identifier function. All members that map to a given key must still match successfully
1313
//with the matcher that is provided for that key.
1414
AllowDuplicates
15+
//IgnoreUnexportedExtras tells the matcher to ignore extra unexported fields, rather than triggering a failure.
16+
//it is not possible to check the value of unexported fields, so this option is only useful when you want to
17+
//check every exported fields, but you don't care about extra unexported fields.
18+
IgnoreUnexportedExtras
1519
)

0 commit comments

Comments
 (0)