...

Source file src/k8s.io/component-base/logs/api/v1/types_test.go

Documentation: k8s.io/component-base/logs/api/v1

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package v1
    18  
    19  import (
    20  	enjson "encoding/json"
    21  	"fmt"
    22  	"math"
    23  	"reflect"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  	"sigs.k8s.io/json"
    29  
    30  	"k8s.io/apimachinery/pkg/api/resource"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  )
    33  
    34  func TestVModule(t *testing.T) {
    35  	testcases := []struct {
    36  		arg         string
    37  		expectError string
    38  		expectValue VModuleConfiguration
    39  		expectParam string
    40  	}{
    41  		{
    42  			arg: "gopher*=1",
    43  			expectValue: VModuleConfiguration{
    44  				{
    45  					FilePattern: "gopher*",
    46  					Verbosity:   1,
    47  				},
    48  			},
    49  		},
    50  		{
    51  			arg: "foo=1,bar=2",
    52  			expectValue: VModuleConfiguration{
    53  				{
    54  					FilePattern: "foo",
    55  					Verbosity:   1,
    56  				},
    57  				{
    58  					FilePattern: "bar",
    59  					Verbosity:   2,
    60  				},
    61  			},
    62  		},
    63  		{
    64  			arg: "foo=1,bar=2,",
    65  			expectValue: VModuleConfiguration{
    66  				{
    67  					FilePattern: "foo",
    68  					Verbosity:   1,
    69  				},
    70  				{
    71  					FilePattern: "bar",
    72  					Verbosity:   2,
    73  				},
    74  			},
    75  			expectParam: "foo=1,bar=2",
    76  		},
    77  		{
    78  			arg:         "gopher*",
    79  			expectError: `"gopher*" does not have the pattern=N format`,
    80  		},
    81  		{
    82  			arg:         "=1",
    83  			expectError: `"=1" does not have the pattern=N format`,
    84  		},
    85  		{
    86  			arg:         "foo=-1",
    87  			expectError: `parsing verbosity in "foo=-1": strconv.ParseUint: parsing "-1": invalid syntax`,
    88  		},
    89  		{
    90  			arg: fmt.Sprintf("validint32=%d", math.MaxInt32),
    91  			expectValue: VModuleConfiguration{
    92  				{
    93  					FilePattern: "validint32",
    94  					Verbosity:   math.MaxInt32,
    95  				},
    96  			},
    97  		},
    98  		{
    99  			arg:         fmt.Sprintf("invalidint32=%d", math.MaxInt32+1),
   100  			expectError: `parsing verbosity in "invalidint32=2147483648": strconv.ParseUint: parsing "2147483648": value out of range`,
   101  		},
   102  	}
   103  
   104  	for _, test := range testcases {
   105  		t.Run(test.arg, func(t *testing.T) {
   106  			var actual VModuleConfiguration
   107  			value := VModuleConfigurationPflag(&actual)
   108  			err := value.Set(test.arg)
   109  			if test.expectError != "" {
   110  				if err == nil {
   111  					t.Fatal("parsing should have failed")
   112  				}
   113  				assert.Equal(t, test.expectError, err.Error(), "parse error")
   114  			} else {
   115  				if err != nil {
   116  					t.Fatalf("unexpected error: %v", err)
   117  				}
   118  				param := value.String()
   119  				expectParam := test.expectParam
   120  				if expectParam == "" {
   121  					expectParam = test.arg
   122  				}
   123  				assert.Equal(t, expectParam, param, "encoded parameter value not identical")
   124  			}
   125  		})
   126  	}
   127  }
   128  
   129  // TestCompatibility ensures that a) valid JSON remains valid and has the same
   130  // effect and b) that new fields are covered by the test data.
   131  func TestCompatibility(t *testing.T) {
   132  	testcases := map[string]struct {
   133  		// fixture holds a representation of a LoggingConfiguration struct in JSON format.
   134  		fixture string
   135  		// baseConfig is the struct that Unmarshal writes into.
   136  		baseConfig LoggingConfiguration
   137  		// expectAllFields enables a reflection check to ensure that the
   138  		// result has all fields set.
   139  		expectAllFields bool
   140  		// expectConfig is the intended result.
   141  		expectConfig LoggingConfiguration
   142  	}{
   143  		"defaults": {
   144  			// No changes when nothing is specified.
   145  			fixture:      "{}",
   146  			baseConfig:   *NewLoggingConfiguration(),
   147  			expectConfig: *NewLoggingConfiguration(),
   148  		},
   149  		"all-fields": {
   150  			// The JSON fixture includes all fields. The result
   151  			// must have all fields as non-empty when starting with
   152  			// an empty base, otherwise the fixture is incomplete
   153  			// and must be updated for the test case to pass.
   154  			fixture: `{
   155  	"format": "json",
   156  	"flushFrequency": 1,
   157  	"verbosity": 5,
   158  	"vmodule": [
   159  		{"filePattern": "someFile", "verbosity": 10},
   160  		{"filePattern": "anotherFile", "verbosity": 1}
   161  	],
   162  	"options": {
   163  		"text": {
   164  			"splitStream": true,
   165  			"infoBufferSize": "2048"
   166  		},
   167  		"json": {
   168  			"splitStream": true,
   169  			"infoBufferSize": "1024"
   170  		}
   171  	}
   172  }
   173  `,
   174  			baseConfig:      LoggingConfiguration{},
   175  			expectAllFields: true,
   176  			expectConfig: LoggingConfiguration{
   177  				Format:         JSONLogFormat,
   178  				FlushFrequency: TimeOrMetaDuration{Duration: metav1.Duration{Duration: time.Nanosecond}},
   179  				Verbosity:      VerbosityLevel(5),
   180  				VModule: VModuleConfiguration{
   181  					{
   182  						FilePattern: "someFile",
   183  						Verbosity:   VerbosityLevel(10),
   184  					},
   185  					{
   186  						FilePattern: "anotherFile",
   187  						Verbosity:   VerbosityLevel(1),
   188  					},
   189  				},
   190  				Options: FormatOptions{
   191  					Text: TextOptions{
   192  						OutputRoutingOptions: OutputRoutingOptions{
   193  							SplitStream: true,
   194  							InfoBufferSize: resource.QuantityValue{
   195  								Quantity: *resource.NewQuantity(2048, resource.DecimalSI),
   196  							},
   197  						},
   198  					},
   199  					JSON: JSONOptions{
   200  						OutputRoutingOptions: OutputRoutingOptions{
   201  							SplitStream: true,
   202  							InfoBufferSize: resource.QuantityValue{
   203  								Quantity: *resource.NewQuantity(1024, resource.DecimalSI),
   204  							},
   205  						},
   206  					},
   207  				},
   208  			},
   209  		},
   210  	}
   211  
   212  	for name, tc := range testcases {
   213  		t.Run(name, func(t *testing.T) {
   214  			// Beware, not a deep copy. Different test cases must
   215  			// not share anything.
   216  			config := tc.baseConfig
   217  			if strictErr, err := json.UnmarshalStrict([]byte(tc.fixture), &config); err != nil {
   218  				t.Fatalf("unexpected unmarshal error: %v", err)
   219  			} else if strictErr != nil {
   220  				t.Fatalf("unexpected strict unmarshal error: %v", strictErr)
   221  			}
   222  			// This sets the internal "s" field just like unmarshaling does.
   223  			// Required for assert.Equal to pass.
   224  			_ = tc.expectConfig.Options.Text.InfoBufferSize.String()
   225  			_ = tc.expectConfig.Options.JSON.InfoBufferSize.String()
   226  			assert.Equal(t, tc.expectConfig, config)
   227  			if tc.expectAllFields {
   228  				notZeroRecursive(t, config, "LoggingConfiguration")
   229  			}
   230  		})
   231  	}
   232  }
   233  
   234  // notZero asserts that i is not the zero value for its type
   235  // and repeats that check recursively for all pointers,
   236  // structs, maps, arrays, and slices.
   237  func notZeroRecursive(t *testing.T, i interface{}, path string) bool {
   238  	typeOfI := reflect.TypeOf(i)
   239  
   240  	if i == nil || reflect.DeepEqual(i, reflect.Zero(typeOfI).Interface()) {
   241  		t.Errorf("%s: should not have been zero, but was %v", path, i)
   242  		return false
   243  	}
   244  
   245  	valid := true
   246  	kind := typeOfI.Kind()
   247  	value := reflect.ValueOf(i)
   248  	switch kind {
   249  	case reflect.Pointer:
   250  		if !notZeroRecursive(t, value.Elem().Interface(), path) {
   251  			valid = false
   252  		}
   253  	case reflect.Struct:
   254  		for i := 0; i < typeOfI.NumField(); i++ {
   255  			if !typeOfI.Field(i).IsExported() {
   256  				// Cannot access value.
   257  				continue
   258  			}
   259  			if typeOfI.Field(i).Tag.Get("json") == "-" {
   260  				// unserialized field
   261  				continue
   262  			}
   263  			if !notZeroRecursive(t, value.Field(i).Interface(), path+"."+typeOfI.Field(i).Name) {
   264  				valid = false
   265  			}
   266  		}
   267  	case reflect.Map:
   268  		iter := value.MapRange()
   269  		for iter.Next() {
   270  			k := iter.Key()
   271  			v := iter.Value()
   272  			if !notZeroRecursive(t, k.Interface(), path+"."+"<key>") {
   273  				valid = false
   274  			}
   275  			if !notZeroRecursive(t, v.Interface(), path+"["+fmt.Sprintf("%v", k.Interface())+"]") {
   276  				valid = false
   277  			}
   278  		}
   279  	case reflect.Slice, reflect.Array:
   280  		for i := 0; i < value.Len(); i++ {
   281  			if !notZeroRecursive(t, value.Index(i).Interface(), path+"["+fmt.Sprintf("%d", i)+"]") {
   282  				valid = false
   283  			}
   284  		}
   285  	}
   286  
   287  	return valid
   288  }
   289  
   290  func TestTimeOrMetaDuration_UnmarshalJSON(t *testing.T) {
   291  	tests := []struct {
   292  		name   string
   293  		tomd   *TimeOrMetaDuration
   294  		arg    any
   295  		wanted string
   296  	}{
   297  		{
   298  			name:   "string values unmarshal as metav1.Duration",
   299  			tomd:   &TimeOrMetaDuration{},
   300  			arg:    "1s",
   301  			wanted: `"1s"`,
   302  		}, {
   303  			name:   "int values unmarshal as metav1.Duration",
   304  			tomd:   &TimeOrMetaDuration{},
   305  			arg:    1000000000,
   306  			wanted: `1000000000`,
   307  		}, {
   308  			name:   "invalid value return error",
   309  			tomd:   &TimeOrMetaDuration{},
   310  			arg:    "invalid",
   311  			wanted: "time: invalid duration \"invalid\"",
   312  		},
   313  	}
   314  	for _, tt := range tests {
   315  		t.Run(tt.name, func(t *testing.T) {
   316  			b, err := enjson.Marshal(tt.arg)
   317  			if err != nil {
   318  				t.Errorf("unexpect error: %v", err)
   319  			}
   320  
   321  			if err := tt.tomd.UnmarshalJSON(b); err == nil {
   322  				data, err := tt.tomd.MarshalJSON()
   323  				if err != nil {
   324  					t.Fatal(err)
   325  				}
   326  				if tt.wanted != string(data) {
   327  					t.Errorf("unexpected wanted for %s, wanted: %v, got: %v", tt.name, tt.wanted, string(data))
   328  				}
   329  			} else {
   330  				if err.Error() != tt.wanted {
   331  					t.Errorf("UnmarshalJSON() error = %v", err)
   332  				}
   333  			}
   334  		})
   335  	}
   336  
   337  }
   338  

View as plain text