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 }