k8s.io/apiserver@v0.31.1/pkg/cel/library/quantity_test.go (about)

     1  /*
     2  Copyright 2023 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 library_test
    18  
    19  import (
    20  	"regexp"
    21  	"testing"
    22  
    23  	"github.com/google/cel-go/cel"
    24  	"github.com/google/cel-go/common"
    25  	"github.com/google/cel-go/common/types"
    26  	"github.com/google/cel-go/common/types/ref"
    27  	"github.com/google/cel-go/ext"
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/stretchr/testify/require"
    30  
    31  	"k8s.io/apimachinery/pkg/api/resource"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	apiservercel "k8s.io/apiserver/pkg/cel"
    34  	"k8s.io/apiserver/pkg/cel/library"
    35  )
    36  
    37  func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
    38  	env, err := cel.NewEnv(
    39  		cel.OptionalTypes(),
    40  		ext.Strings(),
    41  		library.URLs(),
    42  		library.Regex(),
    43  		library.Lists(),
    44  		library.Quantity(),
    45  		library.Format(),
    46  	)
    47  	if err != nil {
    48  		t.Fatalf("%v", err)
    49  	}
    50  	compiled, issues := env.Compile(expr)
    51  
    52  	if len(expectCompileErrs) > 0 {
    53  		missingCompileErrs := []string{}
    54  		matchedCompileErrs := sets.New[int]()
    55  		for _, expectedCompileErr := range expectCompileErrs {
    56  			compiledPattern, err := regexp.Compile(expectedCompileErr)
    57  			if err != nil {
    58  				t.Fatalf("failed to compile expected err regex: %v", err)
    59  			}
    60  
    61  			didMatch := false
    62  
    63  			for i, compileError := range issues.Errors() {
    64  				if compiledPattern.Match([]byte(compileError.Message)) {
    65  					didMatch = true
    66  					matchedCompileErrs.Insert(i)
    67  				}
    68  			}
    69  
    70  			if !didMatch {
    71  				missingCompileErrs = append(missingCompileErrs, expectedCompileErr)
    72  			} else if len(matchedCompileErrs) != len(issues.Errors()) {
    73  				unmatchedErrs := []cel.Error{}
    74  				for i, issue := range issues.Errors() {
    75  					if !matchedCompileErrs.Has(i) {
    76  						unmatchedErrs = append(unmatchedErrs, *issue)
    77  					}
    78  				}
    79  				require.Empty(t, unmatchedErrs, "unexpected compilation errors")
    80  			}
    81  		}
    82  
    83  		require.Empty(t, missingCompileErrs, "expected compilation errors")
    84  		return
    85  	} else if len(issues.Errors()) > 0 {
    86  		errorStrings := []string{}
    87  		source := common.NewTextSource(expr)
    88  		for _, issue := range issues.Errors() {
    89  			errorStrings = append(errorStrings, issue.ToDisplayString(source))
    90  		}
    91  		t.Fatalf("%v", errorStrings)
    92  	}
    93  
    94  	// Typecheck expression
    95  	_, err = cel.AstToCheckedExpr(compiled)
    96  	if err != nil {
    97  		t.Fatalf("%v", err)
    98  	}
    99  
   100  	prog, err := env.Program(compiled)
   101  	if err != nil {
   102  		t.Fatalf("%v", err)
   103  	}
   104  	res, _, err := prog.Eval(map[string]interface{}{})
   105  	if len(expectRuntimeErrPattern) > 0 {
   106  		if err == nil {
   107  			t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern)
   108  		} else if matched, regexErr := regexp.MatchString(expectRuntimeErrPattern, err.Error()); regexErr != nil {
   109  			t.Fatalf("failed to compile expected err regex: %v", regexErr)
   110  		} else if !matched {
   111  			t.Fatalf("unexpected err: %v", err)
   112  		}
   113  	} else if err != nil {
   114  		t.Fatalf("%v", err)
   115  	} else if expectResult != nil {
   116  		converted := res.Equal(expectResult).Value().(bool)
   117  		require.True(t, converted, "expectation not equal to output: %v", cmp.Diff(expectResult.Value(), res.Value()))
   118  	} else {
   119  		t.Fatal("expected result must not be nil")
   120  	}
   121  
   122  }
   123  
   124  func TestQuantity(t *testing.T) {
   125  	twelveMi := resource.MustParse("12Mi")
   126  	trueVal := types.Bool(true)
   127  	falseVal := types.Bool(false)
   128  
   129  	cases := []struct {
   130  		name               string
   131  		expr               string
   132  		expectValue        ref.Val
   133  		expectedCompileErr []string
   134  		expectedRuntimeErr string
   135  	}{
   136  		{
   137  			name:        "parse",
   138  			expr:        `quantity("12Mi")`,
   139  			expectValue: apiservercel.Quantity{Quantity: &twelveMi},
   140  		},
   141  		{
   142  			name:               "parseInvalidSuffix",
   143  			expr:               `quantity("10Mo")`,
   144  			expectedRuntimeErr: "quantities must match the regular expression.*",
   145  		},
   146  		{
   147  			// The above case fails due to a regex check. This case passes the
   148  			// regex check and fails a suffix check
   149  			name:               "parseInvalidSuffixPassesRegex",
   150  			expr:               `quantity("10Mm")`,
   151  			expectedRuntimeErr: "unable to parse quantity's suffix",
   152  		},
   153  		{
   154  			name:        "isQuantity",
   155  			expr:        `isQuantity("20")`,
   156  			expectValue: trueVal,
   157  		},
   158  		{
   159  			name:        "isQuantity_megabytes",
   160  			expr:        `isQuantity("20M")`,
   161  			expectValue: trueVal,
   162  		},
   163  		{
   164  			name:        "isQuantity_mebibytes",
   165  			expr:        `isQuantity("20Mi")`,
   166  			expectValue: trueVal,
   167  		},
   168  		{
   169  			name:        "isQuantity_invalidSuffix",
   170  			expr:        `isQuantity("20Mo")`,
   171  			expectValue: falseVal,
   172  		},
   173  		{
   174  			name:        "isQuantity_passingRegex",
   175  			expr:        `isQuantity("10Mm")`,
   176  			expectValue: falseVal,
   177  		},
   178  		{
   179  			name:               "isQuantity_noOverload",
   180  			expr:               `isQuantity([1, 2, 3])`,
   181  			expectedCompileErr: []string{"found no matching overload for 'isQuantity' applied to.*"},
   182  		},
   183  		{
   184  			name:        "equality_reflexivity",
   185  			expr:        `quantity("200M") == quantity("200M")`,
   186  			expectValue: trueVal,
   187  		},
   188  		{
   189  			name:        "equality_symmetry",
   190  			expr:        `quantity("200M") == quantity("0.2G") && quantity("0.2G") == quantity("200M")`,
   191  			expectValue: trueVal,
   192  		},
   193  		{
   194  			name:        "equality_transitivity",
   195  			expr:        `quantity("2M") == quantity("0.002G") && quantity("2000k") == quantity("2M") && quantity("0.002G") == quantity("2000k")`,
   196  			expectValue: trueVal,
   197  		},
   198  		{
   199  			name:        "inequality",
   200  			expr:        `quantity("200M") == quantity("0.3G")`,
   201  			expectValue: falseVal,
   202  		},
   203  		{
   204  			name:        "quantity_less",
   205  			expr:        `quantity("50M").isLessThan(quantity("50Mi"))`,
   206  			expectValue: trueVal,
   207  		},
   208  		{
   209  			name:        "quantity_less_obvious",
   210  			expr:        `quantity("50M").isLessThan(quantity("100M"))`,
   211  			expectValue: trueVal,
   212  		},
   213  		{
   214  			name:        "quantity_less_false",
   215  			expr:        `quantity("100M").isLessThan(quantity("50M"))`,
   216  			expectValue: falseVal,
   217  		},
   218  		{
   219  			name:        "quantity_greater",
   220  			expr:        `quantity("50Mi").isGreaterThan(quantity("50M"))`,
   221  			expectValue: trueVal,
   222  		},
   223  		{
   224  			name:        "quantity_greater_obvious",
   225  			expr:        `quantity("150Mi").isGreaterThan(quantity("100Mi"))`,
   226  			expectValue: trueVal,
   227  		},
   228  		{
   229  			name:        "quantity_greater_false",
   230  			expr:        `quantity("50M").isGreaterThan(quantity("100M"))`,
   231  			expectValue: falseVal,
   232  		},
   233  		{
   234  			name:        "compare_equal",
   235  			expr:        `quantity("200M").compareTo(quantity("0.2G"))`,
   236  			expectValue: types.Int(0),
   237  		},
   238  		{
   239  			name:        "compare_less",
   240  			expr:        `quantity("50M").compareTo(quantity("50Mi"))`,
   241  			expectValue: types.Int(-1),
   242  		},
   243  		{
   244  			name:        "compare_greater",
   245  			expr:        `quantity("50Mi").compareTo(quantity("50M"))`,
   246  			expectValue: types.Int(1),
   247  		},
   248  		{
   249  			name:        "add_quantity",
   250  			expr:        `quantity("50k").add(quantity("20")) == quantity("50.02k")`,
   251  			expectValue: trueVal,
   252  		},
   253  		{
   254  			name:        "add_int",
   255  			expr:        `quantity("50k").add(20).isLessThan(quantity("50020"))`,
   256  			expectValue: falseVal,
   257  		},
   258  		{
   259  			name:        "sub_quantity",
   260  			expr:        `quantity("50k").sub(quantity("20")) == quantity("49.98k")`,
   261  			expectValue: trueVal,
   262  		},
   263  		{
   264  			name:        "sub_int",
   265  			expr:        `quantity("50k").sub(20) == quantity("49980")`,
   266  			expectValue: trueVal,
   267  		},
   268  		{
   269  			name:        "arith_chain_1",
   270  			expr:        `quantity("50k").add(20).sub(quantity("100k")).asInteger()`,
   271  			expectValue: types.Int(-49980),
   272  		},
   273  		{
   274  			name:        "arith_chain",
   275  			expr:        `quantity("50k").add(20).sub(quantity("100k")).sub(-50000).asInteger()`,
   276  			expectValue: types.Int(20),
   277  		},
   278  		{
   279  			name:        "as_integer",
   280  			expr:        `quantity("50k").asInteger()`,
   281  			expectValue: types.Int(50000),
   282  		},
   283  		{
   284  			name:               "as_integer_error",
   285  			expr:               `quantity("9999999999999999999999999999999999999G").asInteger()`,
   286  			expectedRuntimeErr: `cannot convert value to integer`,
   287  		},
   288  		{
   289  			name:        "is_integer",
   290  			expr:        `quantity("9999999999999999999999999999999999999G").isInteger()`,
   291  			expectValue: falseVal,
   292  		},
   293  		{
   294  			name:        "is_integer",
   295  			expr:        `quantity("50").isInteger()`,
   296  			expectValue: trueVal,
   297  		},
   298  		{
   299  			name:        "as_float",
   300  			expr:        `quantity("50.703k").asApproximateFloat()`,
   301  			expectValue: types.Double(50703),
   302  		},
   303  	}
   304  
   305  	for _, c := range cases {
   306  		t.Run(c.name, func(t *testing.T) {
   307  			testQuantity(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr)
   308  		})
   309  	}
   310  }