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  }