github.com/Jeffail/benthos/v3@v3.65.0/lib/config/docs_test.go (about) 1 package config_test 2 3 import ( 4 "fmt" 5 "reflect" 6 "strings" 7 "testing" 8 9 "github.com/Jeffail/benthos/v3/internal/bundle" 10 "github.com/Jeffail/benthos/v3/internal/docs" 11 "github.com/Jeffail/benthos/v3/lib/buffer" 12 "github.com/Jeffail/benthos/v3/lib/cache" 13 "github.com/Jeffail/benthos/v3/lib/config" 14 "github.com/Jeffail/benthos/v3/lib/input" 15 "github.com/Jeffail/benthos/v3/lib/metrics" 16 "github.com/Jeffail/benthos/v3/lib/output" 17 "github.com/Jeffail/benthos/v3/lib/processor" 18 "github.com/Jeffail/benthos/v3/lib/ratelimit" 19 "github.com/Jeffail/benthos/v3/lib/tracer" 20 uconfig "github.com/Jeffail/benthos/v3/lib/util/config" 21 _ "github.com/Jeffail/benthos/v3/public/components/all" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 ) 25 26 func walkSpecWithConfig(t *testing.T, prefix string, spec docs.FieldSpec, conf interface{}) { 27 t.Helper() 28 29 if _, isCore := spec.Type.IsCoreComponent(); isCore { 30 return 31 } 32 33 if spec.Kind == docs.Kind2DArray { 34 arr, ok := conf.([]interface{}) 35 if !assert.True(t, ok || spec.IsDeprecated, "%v: documented as array but is %T", prefix, conf) { 36 return 37 } 38 for i, ele := range arr { 39 tmpSpec := spec 40 tmpSpec.Kind = docs.KindArray 41 walkSpecWithConfig(t, prefix+fmt.Sprintf("[%v]", i), tmpSpec, ele) 42 } 43 } else if spec.Kind == docs.KindArray { 44 arr, ok := conf.([]interface{}) 45 if !assert.True(t, ok || spec.IsDeprecated, "%v: documented as array but is %T", prefix, conf) { 46 return 47 } 48 for i, ele := range arr { 49 tmpSpec := spec 50 tmpSpec.Kind = docs.KindScalar 51 walkSpecWithConfig(t, prefix+fmt.Sprintf("[%v]", i), tmpSpec, ele) 52 } 53 } else if spec.Kind == docs.KindMap { 54 obj, ok := conf.(map[string]interface{}) 55 if !assert.True(t, ok || spec.IsDeprecated, "%v: documented as map but is %T", prefix, conf) { 56 return 57 } 58 for k, v := range obj { 59 tmpSpec := spec 60 tmpSpec.Kind = docs.KindScalar 61 walkSpecWithConfig(t, prefix+fmt.Sprintf(".<%v>", k), tmpSpec, v) 62 } 63 } else if len(spec.Children) > 0 { 64 obj, ok := conf.(map[string]interface{}) 65 if !assert.True(t, ok, "%v: documented with children but is %T", prefix, conf) { 66 return 67 } 68 for _, child := range spec.Children { 69 c, ok := obj[child.Name] 70 if assert.True(t, ok || child.IsDeprecated, "%v: field documented but not found in config", prefix+"."+child.Name) { 71 walkSpecWithConfig(t, prefix+"."+child.Name, child, c) 72 } 73 delete(obj, child.Name) 74 } 75 if !spec.IsDeprecated { 76 for k := range obj { 77 t.Errorf("%v: field found in config but not documented", prefix+"."+k) 78 } 79 } 80 } else if spec.Type == docs.FieldTypeObject { 81 obj, ok := conf.(map[string]interface{}) 82 if !assert.True(t, ok || conf == nil, "%v: documented as object but is %T", prefix, conf) { 83 return 84 } 85 if len(obj) > 0 && !spec.IsDeprecated { 86 childKeys := []string{} 87 for k := range obj { 88 childKeys = append(childKeys, k) 89 } 90 t.Errorf("%v: documented as object with no children but has: %v", prefix, childKeys) 91 } 92 } else { 93 var isCorrect bool 94 switch spec.Type { 95 case docs.FieldTypeBool: 96 _, isCorrect = conf.(bool) 97 case docs.FieldTypeString: 98 _, isCorrect = conf.(string) 99 case docs.FieldTypeFloat, docs.FieldTypeInt: 100 switch conf.(type) { 101 case int64, int, float64: 102 isCorrect = true 103 } 104 case docs.FieldTypeUnknown: 105 isCorrect = true 106 default: 107 isCorrect = true 108 } 109 assert.True(t, isCorrect || spec.IsDeprecated, "%v: documented as %v but is %T", prefix, spec.Type, conf) 110 } 111 } 112 113 func getFieldByYAMLTag(t reflect.Type, tag string) (reflect.Type, bool) { 114 for i := 0; i < t.NumField(); i++ { 115 field := t.Field(i) 116 yTag := field.Tag.Get("yaml") 117 if yTag == tag { 118 return field.Type, true 119 } 120 } 121 return nil, false 122 } 123 124 func getFieldsByYAMLTag(t reflect.Type) map[string]reflect.Type { 125 tagToField := map[string]reflect.Type{} 126 for i := 0; i < t.NumField(); i++ { 127 field := t.Field(i) 128 tagV := field.Tag.Get("yaml") 129 if tagV == ",inline" { 130 for k, v := range getFieldsByYAMLTag(field.Type) { 131 tagToField[k] = v 132 } 133 } else if tagV != "" { 134 tagV = strings.TrimSuffix(tagV, ",omitempty") 135 tagToField[tagV] = field.Type 136 } 137 } 138 return tagToField 139 } 140 141 func walkTypeWithConfig(t *testing.T, prefix string, spec docs.FieldSpec, v reflect.Type) { 142 t.Helper() 143 144 if _, isCore := spec.Type.IsCoreComponent(); isCore { 145 return 146 } 147 148 if spec.Kind == docs.Kind2DArray { 149 if !assert.True(t, v.Kind() == reflect.Slice, "%v: documented as array but is %v", prefix, v.Kind()) { 150 return 151 } 152 eleSpec := spec 153 eleSpec.Kind = docs.KindArray 154 walkTypeWithConfig(t, prefix+"[]", eleSpec, v.Elem()) 155 } else if spec.Kind == docs.KindArray { 156 if !assert.True(t, v.Kind() == reflect.Slice, "%v: documented as array but is %v", prefix, v.Kind()) { 157 return 158 } 159 eleSpec := spec 160 eleSpec.Kind = docs.KindScalar 161 walkTypeWithConfig(t, prefix+"[]", eleSpec, v.Elem()) 162 } else if spec.Kind == docs.KindMap { 163 if !assert.True(t, v.Kind() == reflect.Map, "%v: documented as map but is %v", prefix, v.Kind()) { 164 return 165 } 166 eleSpec := spec 167 eleSpec.Kind = docs.KindScalar 168 walkTypeWithConfig(t, prefix+"<>", eleSpec, v.Elem()) 169 } else if len(spec.Children) > 0 { 170 fieldByYAMLTag := getFieldsByYAMLTag(v) 171 for _, child := range spec.Children { 172 field, ok := fieldByYAMLTag[child.Name] 173 if assert.True(t, ok, "%v: field documented but not found in config", prefix+"."+child.Name) { 174 walkTypeWithConfig(t, prefix+"."+child.Name, child, field) 175 } 176 delete(fieldByYAMLTag, child.Name) 177 } 178 for k := range fieldByYAMLTag { 179 t.Errorf("%v: field found in config but not documented", prefix+"."+k) 180 } 181 } else if spec.Type == docs.FieldTypeObject { 182 if !assert.True(t, v.Kind() == reflect.Map || v.Kind() == reflect.Struct, "%v: documented as map but is %v", prefix, v.Kind()) { 183 return 184 } 185 if v.Kind() == reflect.Struct && !spec.IsDeprecated { 186 for i := 0; i < v.NumField(); i++ { 187 t.Errorf("%v: documented as object with no children but has: %v", prefix, v.Field(i).Tag.Get("yaml")) 188 } 189 } 190 } else { 191 var isCorrect bool 192 switch spec.Type { 193 case docs.FieldTypeBool: 194 isCorrect = v.Kind() == reflect.Bool 195 case docs.FieldTypeString: 196 isCorrect = v.Kind() == reflect.String 197 case docs.FieldTypeFloat: 198 isCorrect = v.Kind() == reflect.Float64 || 199 v.Kind() == reflect.Float32 200 case docs.FieldTypeInt: 201 isCorrect = v.Kind() == reflect.Int || 202 v.Kind() == reflect.Int64 || 203 v.Kind() == reflect.Int32 || 204 v.Kind() == reflect.Int16 || 205 v.Kind() == reflect.Int8 || 206 v.Kind() == reflect.Uint64 || 207 v.Kind() == reflect.Uint32 || 208 v.Kind() == reflect.Uint16 || 209 v.Kind() == reflect.Uint8 210 case docs.FieldTypeUnknown: 211 isCorrect = v.Kind() == reflect.Interface 212 default: 213 isCorrect = false 214 } 215 assert.True(t, isCorrect || spec.IsDeprecated, "%v: documented as %v but is %v", prefix, spec.Type, v.Kind()) 216 217 if _, isCore := spec.Type.IsCoreComponent(); !isCore && !spec.IsDeprecated { 218 assert.NotNil(t, spec.Default, "%v: struct config fields should always have a default", prefix) 219 } 220 } 221 } 222 223 func TestDocumentationCoverage(t *testing.T) { 224 t.Run("root", func(t *testing.T) { 225 conf := config.New() 226 tConf := reflect.TypeOf(conf) 227 228 spec := docs.FieldCommon("", "").WithChildren(config.Spec()...) 229 walkTypeWithConfig(t, "root", spec, tConf) 230 }) 231 232 t.Run("buffers", func(t *testing.T) { 233 conf := buffer.NewConfig() 234 tConf := reflect.TypeOf(conf) 235 for _, v := range bundle.AllBuffers.Docs() { 236 if v.Plugin { 237 continue 238 } 239 conf.Type = v.Name 240 confSanit, err := conf.Sanitised(false) 241 require.NoError(t, err) 242 walkSpecWithConfig(t, "buffer."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 243 244 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 245 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 246 walkTypeWithConfig(t, "buffer."+v.Name, v.Config, cConf) 247 } 248 } 249 }) 250 251 t.Run("caches", func(t *testing.T) { 252 conf := cache.NewConfig() 253 tConf := reflect.TypeOf(conf) 254 for _, v := range bundle.AllCaches.Docs() { 255 if v.Plugin { 256 continue 257 } 258 conf.Type = v.Name 259 confSanit, err := conf.Sanitised(false) 260 require.NoError(t, err) 261 walkSpecWithConfig(t, "cache."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 262 263 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 264 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 265 walkTypeWithConfig(t, "cache."+v.Name, v.Config, cConf) 266 } 267 } 268 }) 269 270 t.Run("inputs", func(t *testing.T) { 271 conf := input.NewConfig() 272 tConf := reflect.TypeOf(conf) 273 for _, v := range bundle.AllInputs.Docs() { 274 if v.Plugin { 275 continue 276 } 277 conf.Type = v.Name 278 confSanit, err := conf.Sanitised(false) 279 require.NoError(t, err) 280 walkSpecWithConfig(t, "input."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 281 282 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 283 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 284 walkTypeWithConfig(t, "input."+v.Name, v.Config, cConf) 285 } 286 } 287 }) 288 289 t.Run("metrics", func(t *testing.T) { 290 conf := metrics.NewConfig() 291 tConf := reflect.TypeOf(conf) 292 for _, v := range bundle.AllMetrics.Docs() { 293 if v.Plugin { 294 continue 295 } 296 conf.Type = v.Name 297 confSanit, err := conf.Sanitised(false) 298 require.NoError(t, err) 299 walkSpecWithConfig(t, "metrics."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 300 301 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 302 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 303 walkTypeWithConfig(t, "metrics."+v.Name, v.Config, cConf) 304 } 305 } 306 }) 307 308 t.Run("outputs", func(t *testing.T) { 309 conf := output.NewConfig() 310 tConf := reflect.TypeOf(conf) 311 for _, v := range bundle.AllOutputs.Docs() { 312 if v.Plugin { 313 continue 314 } 315 conf.Type = v.Name 316 confSanit, err := conf.Sanitised(false) 317 require.NoError(t, err) 318 walkSpecWithConfig(t, "output."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 319 320 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 321 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 322 walkTypeWithConfig(t, "output."+v.Name, v.Config, cConf) 323 } 324 } 325 }) 326 327 t.Run("processors", func(t *testing.T) { 328 conf := processor.NewConfig() 329 tConf := reflect.TypeOf(conf) 330 for _, v := range bundle.AllProcessors.Docs() { 331 if v.Plugin { 332 continue 333 } 334 conf.Type = v.Name 335 confSanit, err := conf.Sanitised(false) 336 require.NoError(t, err) 337 walkSpecWithConfig(t, "processor."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 338 339 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 340 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 341 walkTypeWithConfig(t, "processor."+v.Name, v.Config, cConf) 342 } 343 } 344 }) 345 346 t.Run("rate limits", func(t *testing.T) { 347 conf := ratelimit.NewConfig() 348 tConf := reflect.TypeOf(conf) 349 for _, v := range bundle.AllRateLimits.Docs() { 350 if v.Plugin { 351 continue 352 } 353 conf.Type = v.Name 354 confSanit, err := conf.Sanitised(false) 355 require.NoError(t, err) 356 walkSpecWithConfig(t, "rate_limit."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 357 358 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 359 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 360 walkTypeWithConfig(t, "rate_limit."+v.Name, v.Config, cConf) 361 } 362 } 363 }) 364 365 t.Run("tracers", func(t *testing.T) { 366 conf := tracer.NewConfig() 367 tConf := reflect.TypeOf(conf) 368 for _, v := range bundle.AllTracers.Docs() { 369 conf.Type = v.Name 370 confSanit, err := conf.Sanitised(false) 371 require.NoError(t, err) 372 walkSpecWithConfig(t, "tracer."+v.Name, v.Config, confSanit.(uconfig.Sanitised)[v.Name]) 373 374 cConf, ok := getFieldByYAMLTag(tConf, v.Name) 375 if v.Status != docs.StatusDeprecated && assert.True(t, ok, v.Name) { 376 walkTypeWithConfig(t, "tracer."+v.Name, v.Config, cConf) 377 } 378 } 379 }) 380 }