k8s.io/apiserver@v0.31.1/pkg/cel/library/cost_test.go (about) 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 library 18 19 import ( 20 "context" 21 "fmt" 22 "testing" 23 24 "github.com/google/cel-go/cel" 25 "github.com/google/cel-go/checker" 26 "github.com/google/cel-go/common" 27 "github.com/google/cel-go/common/ast" 28 "github.com/google/cel-go/common/types" 29 "github.com/google/cel-go/ext" 30 exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" 31 32 "k8s.io/apiserver/pkg/authorization/authorizer" 33 ) 34 35 const ( 36 intListLiteral = "[1, 2, 3, 4, 5]" 37 uintListLiteral = "[uint(1), uint(2), uint(3), uint(4), uint(5)]" 38 doubleListLiteral = "[1.0, 2.0, 3.0, 4.0, 5.0]" 39 boolListLiteral = "[false, true, false, true, false]" 40 stringListLiteral = "['012345678901', '012345678901', '012345678901', '012345678901', '012345678901']" 41 bytesListLiteral = "[bytes('012345678901'), bytes('012345678901'), bytes('012345678901'), bytes('012345678901'), bytes('012345678901')]" 42 durationListLiteral = "[duration('1s'), duration('2s'), duration('3s'), duration('4s'), duration('5s')]" 43 timestampListLiteral = "[timestamp('2011-01-01T00:00:00.000+01:00'), timestamp('2011-01-02T00:00:00.000+01:00'), " + 44 "timestamp('2011-01-03T00:00:00.000+01:00'), timestamp('2011-01-04T00:00:00.000+01:00'), " + 45 "timestamp('2011-01-05T00:00:00.000+01:00')]" 46 stringLiteral = "'01234567890123456789012345678901234567890123456789'" 47 ) 48 49 type comparableCost struct { 50 comparableLiteral string 51 expectedEstimatedCost checker.CostEstimate 52 expectedRuntimeCost uint64 53 54 param string 55 } 56 57 func TestListsCost(t *testing.T) { 58 cases := []struct { 59 opts []string 60 costs []comparableCost 61 }{ 62 { 63 opts: []string{".sum()"}, 64 // 10 cost for the list declaration, the rest is the due to the function call 65 costs: []comparableCost{ 66 { 67 comparableLiteral: intListLiteral, 68 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 69 }, 70 { 71 comparableLiteral: uintListLiteral, 72 expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for casts 73 }, 74 { 75 comparableLiteral: doubleListLiteral, 76 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 77 }, 78 { 79 comparableLiteral: durationListLiteral, 80 expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for casts 81 }, 82 }, 83 }, 84 { 85 opts: []string{".isSorted()", ".max()", ".min()"}, 86 // 10 cost for the list declaration, the rest is the due to the function call 87 costs: []comparableCost{ 88 { 89 comparableLiteral: intListLiteral, 90 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 91 }, 92 { 93 comparableLiteral: uintListLiteral, 94 expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for numeric casts 95 }, 96 { 97 comparableLiteral: doubleListLiteral, 98 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 99 }, 100 { 101 comparableLiteral: boolListLiteral, 102 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 103 }, 104 { 105 comparableLiteral: stringListLiteral, 106 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 25}, expectedRuntimeCost: 15, // +5 for string comparisons 107 }, 108 { 109 comparableLiteral: bytesListLiteral, 110 expectedEstimatedCost: checker.CostEstimate{Min: 25, Max: 35}, expectedRuntimeCost: 25, // +10 for casts from string to byte, +5 for byte comparisons 111 }, 112 { 113 comparableLiteral: durationListLiteral, 114 expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for numeric casts 115 }, 116 { 117 comparableLiteral: timestampListLiteral, 118 expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for casts 119 }, 120 }, 121 }, 122 } 123 for _, tc := range cases { 124 for _, op := range tc.opts { 125 for _, typ := range tc.costs { 126 t.Run(typ.comparableLiteral+op, func(t *testing.T) { 127 e := typ.comparableLiteral + op 128 testCost(t, e, typ.expectedEstimatedCost, typ.expectedRuntimeCost) 129 }) 130 } 131 } 132 } 133 } 134 135 func TestIndexOfCost(t *testing.T) { 136 cases := []struct { 137 opts []string 138 costs []comparableCost 139 }{ 140 { 141 opts: []string{".indexOf(%s)", ".lastIndexOf(%s)"}, 142 // 10 cost for the list declaration, the rest is the due to the function call 143 costs: []comparableCost{ 144 { 145 comparableLiteral: intListLiteral, param: "3", 146 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 147 }, 148 { 149 comparableLiteral: uintListLiteral, param: "uint(3)", 150 expectedEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, expectedRuntimeCost: 21, // +5 for numeric casts 151 }, 152 { 153 comparableLiteral: doubleListLiteral, param: "3.0", 154 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 155 }, 156 { 157 comparableLiteral: boolListLiteral, param: "true", 158 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15, 159 }, 160 { 161 comparableLiteral: stringListLiteral, param: "'x'", 162 expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 25}, expectedRuntimeCost: 15, // +5 for string comparisons 163 }, 164 { 165 comparableLiteral: bytesListLiteral, param: "bytes('x')", 166 expectedEstimatedCost: checker.CostEstimate{Min: 26, Max: 36}, expectedRuntimeCost: 26, // +11 for casts from string to byte, +5 for byte comparisons 167 }, 168 { 169 comparableLiteral: durationListLiteral, param: "duration('3s')", 170 expectedEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, expectedRuntimeCost: 21, // +6 for casts from duration to byte 171 }, 172 { 173 comparableLiteral: timestampListLiteral, param: "timestamp('2011-01-03T00:00:00.000+01:00')", 174 expectedEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, expectedRuntimeCost: 21, // +6 for casts from timestamp to byte 175 }, 176 177 // index of operations are also defined for strings 178 { 179 comparableLiteral: stringLiteral, param: "'123'", 180 expectedEstimatedCost: checker.CostEstimate{Min: 5, Max: 5}, expectedRuntimeCost: 5, 181 }, 182 }, 183 }, 184 } 185 for _, tc := range cases { 186 for _, op := range tc.opts { 187 for _, typ := range tc.costs { 188 opWithParam := fmt.Sprintf(op, typ.param) 189 t.Run(typ.comparableLiteral+opWithParam, func(t *testing.T) { 190 e := typ.comparableLiteral + opWithParam 191 testCost(t, e, typ.expectedEstimatedCost, typ.expectedRuntimeCost) 192 }) 193 } 194 } 195 } 196 } 197 198 func TestURLsCost(t *testing.T) { 199 cases := []struct { 200 ops []string 201 expectEsimatedCost checker.CostEstimate 202 expectRuntimeCost uint64 203 }{ 204 { 205 ops: []string{".getScheme()", ".getHostname()", ".getHost()", ".getPort()", ".getEscapedPath()", ".getQuery()"}, 206 expectEsimatedCost: checker.CostEstimate{Min: 4, Max: 4}, 207 expectRuntimeCost: 4, 208 }, 209 } 210 211 for _, tc := range cases { 212 for _, op := range tc.ops { 213 t.Run("url."+op, func(t *testing.T) { 214 testCost(t, "url('https:://kubernetes.io/')"+op, tc.expectEsimatedCost, tc.expectRuntimeCost) 215 }) 216 } 217 } 218 } 219 220 func TestIPCost(t *testing.T) { 221 ipv4 := "ip('192.168.0.1')" 222 ipv4BaseEstimatedCost := checker.CostEstimate{Min: 2, Max: 2} 223 ipv4BaseRuntimeCost := uint64(2) 224 225 ipv6 := "ip('2001:db8:3333:4444:5555:6666:7777:8888')" 226 ipv6BaseEstimatedCost := checker.CostEstimate{Min: 4, Max: 4} 227 ipv6BaseRuntimeCost := uint64(4) 228 229 testCases := []struct { 230 ops []string 231 expectEsimatedCost func(checker.CostEstimate) checker.CostEstimate 232 expectRuntimeCost func(uint64) uint64 233 }{ 234 { 235 // For just parsing the IP, the cost is expected to be the base. 236 ops: []string{""}, 237 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { return c }, 238 expectRuntimeCost: func(c uint64) uint64 { return c }, 239 }, 240 { 241 ops: []string{".family()", ".isUnspecified()", ".isLoopback()", ".isLinkLocalMulticast()", ".isLinkLocalUnicast()", ".isGlobalUnicast()"}, 242 // For most other operations, the cost is expected to be the base + 1. 243 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 244 return checker.CostEstimate{Min: c.Min + 1, Max: c.Max + 1} 245 }, 246 expectRuntimeCost: func(c uint64) uint64 { return c + 1 }, 247 }, 248 } 249 250 for _, tc := range testCases { 251 for _, op := range tc.ops { 252 t.Run(ipv4+op, func(t *testing.T) { 253 testCost(t, ipv4+op, tc.expectEsimatedCost(ipv4BaseEstimatedCost), tc.expectRuntimeCost(ipv4BaseRuntimeCost)) 254 }) 255 256 t.Run(ipv6+op, func(t *testing.T) { 257 testCost(t, ipv6+op, tc.expectEsimatedCost(ipv6BaseEstimatedCost), tc.expectRuntimeCost(ipv6BaseRuntimeCost)) 258 }) 259 } 260 } 261 } 262 263 func TestIPIsCanonicalCost(t *testing.T) { 264 testCases := []struct { 265 op string 266 expectEsimatedCost checker.CostEstimate 267 expectRuntimeCost uint64 268 }{ 269 { 270 op: "ip.isCanonical('192.168.0.1')", 271 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 272 expectRuntimeCost: 3, 273 }, 274 { 275 op: "ip.isCanonical('2001:db8:3333:4444:5555:6666:7777:8888')", 276 expectEsimatedCost: checker.CostEstimate{Min: 8, Max: 8}, 277 expectRuntimeCost: 8, 278 }, 279 { 280 op: "ip.isCanonical('2001:db8::abcd')", 281 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 282 expectRuntimeCost: 3, 283 }, 284 } 285 286 for _, tc := range testCases { 287 t.Run(tc.op, func(t *testing.T) { 288 testCost(t, tc.op, tc.expectEsimatedCost, tc.expectRuntimeCost) 289 }) 290 } 291 } 292 293 func TestCIDRCost(t *testing.T) { 294 ipv4 := "cidr('192.168.0.0/16')" 295 ipv4BaseEstimatedCost := checker.CostEstimate{Min: 2, Max: 2} 296 ipv4BaseRuntimeCost := uint64(2) 297 298 ipv6 := "cidr('2001:db8::/32')" 299 ipv6BaseEstimatedCost := checker.CostEstimate{Min: 2, Max: 2} 300 ipv6BaseRuntimeCost := uint64(2) 301 302 type testCase struct { 303 ops []string 304 expectEsimatedCost func(checker.CostEstimate) checker.CostEstimate 305 expectRuntimeCost func(uint64) uint64 306 } 307 308 cases := []testCase{ 309 { 310 // For just parsing the IP, the cost is expected to be the base. 311 ops: []string{""}, 312 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { return c }, 313 expectRuntimeCost: func(c uint64) uint64 { return c }, 314 }, 315 { 316 ops: []string{".ip()", ".prefixLength()", ".masked()"}, 317 // For most other operations, the cost is expected to be the base + 1. 318 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 319 return checker.CostEstimate{Min: c.Min + 1, Max: c.Max + 1} 320 }, 321 expectRuntimeCost: func(c uint64) uint64 { return c + 1 }, 322 }, 323 } 324 325 //nolint:gocritic 326 ipv4Cases := append(cases, []testCase{ 327 { 328 ops: []string{".containsCIDR(cidr('192.0.0.0/30'))"}, 329 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 330 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 331 }, 332 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 333 }, 334 { 335 ops: []string{".containsCIDR(cidr('192.168.0.0/16'))"}, 336 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 337 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 338 }, 339 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 340 }, 341 { 342 ops: []string{".containsCIDR('192.0.0.0/30')"}, 343 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 344 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 345 }, 346 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 347 }, 348 { 349 ops: []string{".containsCIDR('192.168.0.0/16')"}, 350 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 351 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 352 }, 353 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 354 }, 355 { 356 ops: []string{".containsIP(ip('192.0.0.1'))"}, 357 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 358 return checker.CostEstimate{Min: c.Min + 2, Max: c.Max + 5} 359 }, 360 expectRuntimeCost: func(c uint64) uint64 { return c + 2 }, 361 }, 362 { 363 ops: []string{".containsIP(ip('192.169.0.1'))"}, 364 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 365 return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6} 366 }, 367 expectRuntimeCost: func(c uint64) uint64 { return c + 3 }, 368 }, 369 { 370 ops: []string{".containsIP(ip('192.169.169.250'))"}, 371 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 372 return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6} 373 }, 374 expectRuntimeCost: func(c uint64) uint64 { return c + 3 }, 375 }, 376 { 377 ops: []string{".containsIP('192.0.0.1')"}, 378 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 379 return checker.CostEstimate{Min: c.Min + 2, Max: c.Max + 5} 380 }, 381 expectRuntimeCost: func(c uint64) uint64 { return c + 2 }, 382 }, 383 { 384 ops: []string{".containsIP('192.169.0.1')"}, 385 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 386 return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6} 387 }, 388 expectRuntimeCost: func(c uint64) uint64 { return c + 3 }, 389 }, 390 }...) 391 392 //nolint:gocritic 393 ipv6Cases := append(cases, []testCase{ 394 { 395 ops: []string{".containsCIDR(cidr('2001:db8::/126'))"}, 396 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 397 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 398 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 399 }, 400 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 401 }, 402 { 403 ops: []string{".containsCIDR(cidr('2001:db8::/32'))"}, 404 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 405 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 406 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 407 }, 408 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 409 }, 410 { 411 ops: []string{".containsCIDR('2001:db8::/126')"}, 412 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 413 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 414 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 415 }, 416 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 417 }, 418 { 419 ops: []string{".containsCIDR('2001:db8::/32')"}, 420 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 421 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 422 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9} 423 }, 424 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 425 }, 426 { 427 ops: []string{".containsIP(ip('2001:db8:3333:4444:5555:6666:7777:8888'))"}, 428 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 429 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 430 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 8} 431 }, 432 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 433 }, 434 { 435 ops: []string{".containsIP(ip('2001:db8::1'))"}, 436 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 437 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 438 return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6} 439 }, 440 expectRuntimeCost: func(c uint64) uint64 { return c + 3 }, 441 }, 442 { 443 ops: []string{".containsIP('2001:db8:3333:4444:5555:6666:7777:8888')"}, 444 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 445 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 446 return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 8} 447 }, 448 expectRuntimeCost: func(c uint64) uint64 { return c + 5 }, 449 }, 450 { 451 ops: []string{".containsIP('2001:db8::1')"}, 452 // For operations like checking if an IP is in a CIDR, the cost is expected to higher. 453 expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { 454 return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6} 455 }, 456 expectRuntimeCost: func(c uint64) uint64 { return c + 3 }, 457 }, 458 }...) 459 460 for _, tc := range ipv4Cases { 461 for _, op := range tc.ops { 462 t.Run(ipv4+op, func(t *testing.T) { 463 testCost(t, ipv4+op, tc.expectEsimatedCost(ipv4BaseEstimatedCost), tc.expectRuntimeCost(ipv4BaseRuntimeCost)) 464 }) 465 } 466 } 467 468 for _, tc := range ipv6Cases { 469 for _, op := range tc.ops { 470 t.Run(ipv6+op, func(t *testing.T) { 471 testCost(t, ipv6+op, tc.expectEsimatedCost(ipv6BaseEstimatedCost), tc.expectRuntimeCost(ipv6BaseRuntimeCost)) 472 }) 473 } 474 } 475 } 476 477 func TestStringLibrary(t *testing.T) { 478 cases := []struct { 479 name string 480 expr string 481 expectEsimatedCost checker.CostEstimate 482 expectRuntimeCost uint64 483 }{ 484 { 485 name: "lowerAscii", 486 expr: "'ABCDEFGHIJ abcdefghij'.lowerAscii()", 487 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 488 expectRuntimeCost: 3, 489 }, 490 { 491 name: "lowerAsciiEquals", 492 expr: "'ABCDEFGHIJ abcdefghij'.lowerAscii() == 'abcdefghij ABCDEFGHIJ'.lowerAscii()", 493 expectEsimatedCost: checker.CostEstimate{Min: 7, Max: 9}, 494 expectRuntimeCost: 9, 495 }, 496 { 497 name: "upperAscii", 498 expr: "'ABCDEFGHIJ abcdefghij'.upperAscii()", 499 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 500 expectRuntimeCost: 3, 501 }, 502 { 503 name: "upperAsciiEquals", 504 expr: "'ABCDEFGHIJ abcdefghij'.upperAscii() == 'abcdefghij ABCDEFGHIJ'.upperAscii()", 505 expectEsimatedCost: checker.CostEstimate{Min: 7, Max: 9}, 506 expectRuntimeCost: 9, 507 }, 508 { 509 name: "quote", 510 expr: "strings.quote('ABCDEFGHIJ abcdefghij')", 511 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 512 expectRuntimeCost: 3, 513 }, 514 { 515 name: "quoteEquals", 516 expr: "strings.quote('ABCDEFGHIJ abcdefghij') == strings.quote('ABCDEFGHIJ abcdefghij')", 517 expectEsimatedCost: checker.CostEstimate{Min: 7, Max: 11}, 518 expectRuntimeCost: 9, 519 }, 520 { 521 name: "replace", 522 expr: "'abc 123 def 123'.replace('123', '456')", 523 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 524 expectRuntimeCost: 3, 525 }, 526 { 527 name: "replace between all chars", 528 expr: "'abc 123 def 123'.replace('', 'x')", 529 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 530 expectRuntimeCost: 3, 531 }, 532 { 533 name: "replace with empty", 534 expr: "'abc 123 def 123'.replace('123', '')", 535 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 536 expectRuntimeCost: 3, 537 }, 538 { 539 name: "replace with limit", 540 expr: "'abc 123 def 123'.replace('123', '456', 1)", 541 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 542 expectRuntimeCost: 3, 543 }, 544 { 545 name: "split", 546 expr: "'abc 123 def 123'.split(' ')", 547 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 548 expectRuntimeCost: 3, 549 }, 550 { 551 name: "split with limit", 552 expr: "'abc 123 def 123'.split(' ', 1)", 553 expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 554 expectRuntimeCost: 3, 555 }, 556 { 557 name: "substring", 558 expr: "'abc 123 def 123'.substring(5)", 559 expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 560 expectRuntimeCost: 2, 561 }, 562 { 563 name: "substring with end", 564 expr: "'abc 123 def 123'.substring(5, 8)", 565 expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 566 expectRuntimeCost: 2, 567 }, 568 { 569 name: "trim", 570 expr: "' abc 123 def 123 '.trim()", 571 expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 572 expectRuntimeCost: 2, 573 }, 574 { 575 name: "join with separator", 576 expr: "['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')", 577 expectEsimatedCost: checker.CostEstimate{Min: 11, Max: 23}, 578 expectRuntimeCost: 15, 579 }, 580 { 581 name: "join", 582 expr: "['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join()", 583 expectEsimatedCost: checker.CostEstimate{Min: 10, Max: 22}, 584 expectRuntimeCost: 13, 585 }, 586 { 587 name: "find", 588 expr: "'abc 123 def 123'.find('123')", 589 expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 590 expectRuntimeCost: 2, 591 }, 592 { 593 name: "findAll", 594 expr: "'abc 123 def 123'.findAll('123')", 595 expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 596 expectRuntimeCost: 2, 597 }, 598 { 599 name: "findAll with limit", 600 expr: "'abc 123 def 123'.findAll('123', 1)", 601 expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 602 expectRuntimeCost: 2, 603 }, 604 } 605 606 for _, tc := range cases { 607 t.Run(tc.name, func(t *testing.T) { 608 testCost(t, tc.expr, tc.expectEsimatedCost, tc.expectRuntimeCost) 609 }) 610 } 611 } 612 613 func TestAuthzLibrary(t *testing.T) { 614 cases := []struct { 615 name string 616 expr string 617 expectEstimatedCost checker.CostEstimate 618 expectRuntimeCost uint64 619 }{ 620 { 621 name: "path", 622 expr: "authorizer.path('/healthz')", 623 expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 624 expectRuntimeCost: 2, 625 }, 626 { 627 name: "resource", 628 expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend')", 629 expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6}, 630 expectRuntimeCost: 6, 631 }, 632 { 633 name: "fieldSelector", 634 expr: "authorizer.group('').resource('pods').fieldSelector('spec.nodeName=example-node-name.fully.qualified.domain.name.example.com')", 635 expectEstimatedCost: checker.CostEstimate{Min: 1821, Max: 1821}, 636 expectRuntimeCost: 1821, // authorizer(1) + group(1) + resource(1) + fieldSelector(10 + ceil(71/2)*50=1800 + ceil(71*.1)=8) 637 }, 638 { 639 name: "labelSelector", 640 expr: "authorizer.group('').resource('pods').labelSelector('spec.nodeName=example-node-name.fully.qualified.domain.name.example.com')", 641 expectEstimatedCost: checker.CostEstimate{Min: 1821, Max: 1821}, 642 expectRuntimeCost: 1821, // authorizer(1) + group(1) + resource(1) + fieldSelector(10 + ceil(71/2)*50=1800 + ceil(71*.1)=8) 643 }, 644 { 645 name: "path check allowed", 646 expr: "authorizer.path('/healthz').check('get').allowed()", 647 expectEstimatedCost: checker.CostEstimate{Min: 350003, Max: 350003}, 648 expectRuntimeCost: 350003, 649 }, 650 { 651 name: "resource check allowed", 652 expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", 653 expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007}, 654 expectRuntimeCost: 350007, 655 }, 656 { 657 name: "resource check reason", 658 expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", 659 expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007}, 660 expectRuntimeCost: 350007, 661 }, 662 { 663 name: "resource check errored", 664 expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').errored()", 665 expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007}, 666 expectRuntimeCost: 350007, 667 }, 668 { 669 name: "resource check error", 670 expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').error()", 671 expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007}, 672 expectRuntimeCost: 350007, 673 }, 674 } 675 676 for _, tc := range cases { 677 t.Run(tc.name, func(t *testing.T) { 678 testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost) 679 }) 680 } 681 } 682 683 func TestQuantityCost(t *testing.T) { 684 cases := []struct { 685 name string 686 expr string 687 expectEstimatedCost checker.CostEstimate 688 expectRuntimeCost uint64 689 }{ 690 { 691 name: "path", 692 expr: `quantity("12Mi")`, 693 expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1}, 694 expectRuntimeCost: 1, 695 }, 696 { 697 name: "isQuantity", 698 expr: `isQuantity("20")`, 699 expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1}, 700 expectRuntimeCost: 1, 701 }, 702 { 703 name: "isQuantity_megabytes", 704 expr: `isQuantity("20M")`, 705 expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1}, 706 expectRuntimeCost: 1, 707 }, 708 { 709 name: "equality_reflexivity", 710 expr: `quantity("200M") == quantity("200M")`, 711 expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 1844674407370955266}, 712 expectRuntimeCost: 3, 713 }, 714 { 715 name: "equality_symmetry", 716 expr: `quantity("200M") == quantity("0.2G") && quantity("0.2G") == quantity("200M")`, 717 expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3689348814741910532}, 718 expectRuntimeCost: 6, 719 }, 720 { 721 name: "equality_transitivity", 722 expr: `quantity("2M") == quantity("0.002G") && quantity("2000k") == quantity("2M") && quantity("0.002G") == quantity("2000k")`, 723 expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 5534023222112865798}, 724 expectRuntimeCost: 9, 725 }, 726 { 727 name: "quantity_less", 728 expr: `quantity("50M").isLessThan(quantity("50Mi"))`, 729 expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 730 expectRuntimeCost: 3, 731 }, 732 { 733 name: "quantity_greater", 734 expr: `quantity("50Mi").isGreaterThan(quantity("50M"))`, 735 expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 736 expectRuntimeCost: 3, 737 }, 738 { 739 name: "compare_equal", 740 expr: `quantity("200M").compareTo(quantity("0.2G")) > 0`, 741 expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4}, 742 expectRuntimeCost: 4, 743 }, 744 { 745 name: "add_quantity", 746 expr: `quantity("50k").add(quantity("20")) == quantity("50.02k")`, 747 expectEstimatedCost: checker.CostEstimate{Min: 5, Max: 1844674407370955268}, 748 expectRuntimeCost: 5, 749 }, 750 { 751 name: "sub_quantity", 752 expr: `quantity("50k").sub(quantity("20")) == quantity("49.98k")`, 753 expectEstimatedCost: checker.CostEstimate{Min: 5, Max: 1844674407370955268}, 754 expectRuntimeCost: 5, 755 }, 756 { 757 name: "sub_int", 758 expr: `quantity("50k").sub(20) == quantity("49980")`, 759 expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 1844674407370955267}, 760 expectRuntimeCost: 4, 761 }, 762 { 763 name: "arith_chain_1", 764 expr: `quantity("50k").add(20).sub(quantity("100k")).asInteger() > 0`, 765 expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6}, 766 expectRuntimeCost: 6, 767 }, 768 { 769 name: "arith_chain", 770 expr: `quantity("50k").add(20).sub(quantity("100k")).sub(-50000).asInteger() > 0`, 771 expectEstimatedCost: checker.CostEstimate{Min: 7, Max: 7}, 772 expectRuntimeCost: 7, 773 }, 774 { 775 name: "as_integer", 776 expr: `quantity("50k").asInteger() > 0`, 777 expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 778 expectRuntimeCost: 3, 779 }, 780 { 781 name: "is_integer", 782 expr: `quantity("50").isInteger()`, 783 expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2}, 784 expectRuntimeCost: 2, 785 }, 786 { 787 name: "as_float", 788 expr: `quantity("50.703k").asApproximateFloat() > 0.0`, 789 expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3}, 790 expectRuntimeCost: 3, 791 }, 792 } 793 794 for _, tc := range cases { 795 t.Run(tc.name, func(t *testing.T) { 796 testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost) 797 }) 798 } 799 } 800 801 func TestNameFormatCost(t *testing.T) { 802 cases := []struct { 803 name string 804 expr string 805 expectEstimatedCost checker.CostEstimate 806 expectRuntimeCost uint64 807 }{ 808 { 809 name: "format.named", 810 expr: `format.named("dns1123subdomain")`, 811 expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1}, 812 expectRuntimeCost: 1, 813 }, 814 { 815 name: "format.dns1123Subdomain.validate", 816 expr: `format.named("dns1123Subdomain").value().validate("my-name")`, 817 // Estimated cost doesnt know value at runtime so it is 818 // using an estimated maximum regex length 819 expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34}, 820 expectRuntimeCost: 17, 821 }, 822 { 823 name: "format.dns1123label.validate", 824 expr: `format.named("dns1123Label").value().validate("my-name")`, 825 expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34}, 826 expectRuntimeCost: 10, 827 }, 828 } 829 830 for _, tc := range cases { 831 t.Run(tc.name, func(t *testing.T) { 832 testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost) 833 }) 834 } 835 } 836 837 func TestSetsCost(t *testing.T) { 838 cases := []struct { 839 name string 840 expr string 841 expectEstimatedCost checker.CostEstimate 842 expectRuntimeCost uint64 843 }{ 844 { 845 name: "sets", 846 expr: `sets.contains([], [])`, 847 expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, 848 expectRuntimeCost: 21, 849 }, 850 { 851 expr: `sets.contains([1], [])`, 852 expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, 853 expectRuntimeCost: 21, 854 }, 855 { 856 expr: `sets.contains([1], [1])`, 857 expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, 858 expectRuntimeCost: 22, 859 }, 860 { 861 expr: `sets.contains([1], [1, 1])`, 862 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 863 expectRuntimeCost: 23, 864 }, 865 { 866 expr: `sets.contains([1, 1], [1])`, 867 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 868 expectRuntimeCost: 23, 869 }, 870 { 871 expr: `sets.contains([2, 1], [1])`, 872 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 873 expectRuntimeCost: 23, 874 }, 875 { 876 expr: `sets.contains([1, 2, 3, 4], [2, 3])`, 877 expectEstimatedCost: checker.CostEstimate{Min: 29, Max: 29}, 878 expectRuntimeCost: 29, 879 }, 880 { 881 expr: `sets.contains([1], [1.0, 1])`, 882 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 883 expectRuntimeCost: 23, 884 }, 885 { 886 expr: `sets.contains([1, 2], [2u, 2.0])`, 887 expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, 888 expectRuntimeCost: 25, 889 }, 890 { 891 expr: `sets.contains([1, 2u], [2, 2.0])`, 892 expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, 893 expectRuntimeCost: 25, 894 }, 895 { 896 expr: `sets.contains([1, 2.0, 3u], [1.0, 2u, 3])`, 897 expectEstimatedCost: checker.CostEstimate{Min: 30, Max: 30}, 898 expectRuntimeCost: 30, 899 }, 900 { 901 expr: `sets.contains([[1], [2, 3]], [[2, 3.0]])`, 902 // 10 for each list creation, top-level list sizes are 2, 1 903 expectEstimatedCost: checker.CostEstimate{Min: 53, Max: 53}, 904 expectRuntimeCost: 53, 905 }, 906 { 907 expr: `!sets.contains([1], [2])`, 908 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 909 expectRuntimeCost: 23, 910 }, 911 { 912 expr: `!sets.contains([1], [1, 2])`, 913 expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, 914 expectRuntimeCost: 24, 915 }, 916 { 917 expr: `!sets.contains([1], ["1", 1])`, 918 expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, 919 expectRuntimeCost: 24, 920 }, 921 { 922 expr: `!sets.contains([1], [1.1, 1u])`, 923 expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, 924 expectRuntimeCost: 24, 925 }, 926 927 // set equivalence (note the cost factor is higher as it's basically two contains checks) 928 { 929 expr: `sets.equivalent([], [])`, 930 expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, 931 expectRuntimeCost: 21, 932 }, 933 { 934 expr: `sets.equivalent([1], [1])`, 935 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 936 expectRuntimeCost: 23, 937 }, 938 { 939 expr: `sets.equivalent([1], [1, 1])`, 940 expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, 941 expectRuntimeCost: 25, 942 }, 943 { 944 expr: `sets.equivalent([1, 1], [1])`, 945 expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, 946 expectRuntimeCost: 25, 947 }, 948 { 949 expr: `sets.equivalent([1], [1u, 1.0])`, 950 expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, 951 expectRuntimeCost: 25, 952 }, 953 { 954 expr: `sets.equivalent([1], [1u, 1.0])`, 955 expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, 956 expectRuntimeCost: 25, 957 }, 958 { 959 expr: `sets.equivalent([1, 2, 3], [3u, 2.0, 1])`, 960 expectEstimatedCost: checker.CostEstimate{Min: 39, Max: 39}, 961 expectRuntimeCost: 39, 962 }, 963 { 964 expr: `sets.equivalent([[1.0], [2, 3]], [[1], [2, 3.0]])`, 965 expectEstimatedCost: checker.CostEstimate{Min: 69, Max: 69}, 966 expectRuntimeCost: 69, 967 }, 968 { 969 expr: `!sets.equivalent([2, 1], [1])`, 970 expectEstimatedCost: checker.CostEstimate{Min: 26, Max: 26}, 971 expectRuntimeCost: 26, 972 }, 973 { 974 expr: `!sets.equivalent([1], [1, 2])`, 975 expectEstimatedCost: checker.CostEstimate{Min: 26, Max: 26}, 976 expectRuntimeCost: 26, 977 }, 978 { 979 expr: `!sets.equivalent([1, 2], [2u, 2, 2.0])`, 980 expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34}, 981 expectRuntimeCost: 34, 982 }, 983 { 984 expr: `!sets.equivalent([1, 2], [1u, 2, 2.3])`, 985 expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34}, 986 expectRuntimeCost: 34, 987 }, 988 { 989 expr: `sets.intersects([1], [1])`, 990 expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, 991 expectRuntimeCost: 22, 992 }, 993 { 994 expr: `sets.intersects([1], [1, 1])`, 995 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 996 expectRuntimeCost: 23, 997 }, 998 { 999 expr: `sets.intersects([1, 1], [1])`, 1000 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 1001 expectRuntimeCost: 23, 1002 }, 1003 { 1004 expr: `sets.intersects([2, 1], [1])`, 1005 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 1006 expectRuntimeCost: 23, 1007 }, 1008 { 1009 expr: `sets.intersects([1], [1, 2])`, 1010 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 1011 expectRuntimeCost: 23, 1012 }, 1013 { 1014 expr: `sets.intersects([1], [1.0, 2])`, 1015 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 1016 expectRuntimeCost: 23, 1017 }, 1018 { 1019 expr: `sets.intersects([1, 2], [2u, 2, 2.0])`, 1020 expectEstimatedCost: checker.CostEstimate{Min: 27, Max: 27}, 1021 expectRuntimeCost: 27, 1022 }, 1023 { 1024 expr: `sets.intersects([1, 2], [1u, 2, 2.3])`, 1025 expectEstimatedCost: checker.CostEstimate{Min: 27, Max: 27}, 1026 expectRuntimeCost: 27, 1027 }, 1028 { 1029 expr: `sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]])`, 1030 expectEstimatedCost: checker.CostEstimate{Min: 65, Max: 65}, 1031 expectRuntimeCost: 65, 1032 }, 1033 { 1034 expr: `!sets.intersects([], [])`, 1035 expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, 1036 expectRuntimeCost: 22, 1037 }, 1038 { 1039 expr: `!sets.intersects([1], [])`, 1040 expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, 1041 expectRuntimeCost: 22, 1042 }, 1043 { 1044 expr: `!sets.intersects([1], [2])`, 1045 expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, 1046 expectRuntimeCost: 23, 1047 }, 1048 { 1049 expr: `!sets.intersects([1], ["1", 2])`, 1050 expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, 1051 expectRuntimeCost: 24, 1052 }, 1053 { 1054 expr: `!sets.intersects([1], [1.1, 2u])`, 1055 expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, 1056 expectRuntimeCost: 24, 1057 }, 1058 } 1059 1060 for _, tc := range cases { 1061 t.Run(tc.name, func(t *testing.T) { 1062 testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost) 1063 }) 1064 } 1065 } 1066 1067 func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) { 1068 originalPanicOnUnknown := panicOnUnknown 1069 panicOnUnknown = true 1070 t.Cleanup(func() { panicOnUnknown = originalPanicOnUnknown }) 1071 1072 est := &CostEstimator{SizeEstimator: &testCostEstimator{}} 1073 env, err := cel.NewEnv( 1074 ext.Strings(ext.StringsVersion(2)), 1075 URLs(), 1076 Regex(), 1077 Lists(), 1078 Authz(), 1079 AuthzSelectors(), 1080 Quantity(), 1081 ext.Sets(), 1082 IP(), 1083 CIDR(), 1084 Format(), 1085 cel.OptionalTypes(), 1086 // cel-go v0.17.7 introduced CostEstimatorOptions. 1087 // Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. 1088 cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)), 1089 ) 1090 if err != nil { 1091 t.Fatalf("%v", err) 1092 } 1093 env, err = env.Extend(cel.Variable("authorizer", AuthorizerType)) 1094 if err != nil { 1095 t.Fatalf("%v", err) 1096 } 1097 compiled, issues := env.Compile(expr) 1098 if len(issues.Errors()) > 0 { 1099 var errList []string 1100 for _, issue := range issues.Errors() { 1101 errList = append(errList, issue.ToDisplayString(common.NewTextSource(expr))) 1102 } 1103 t.Fatalf("%v", errList) 1104 } 1105 estCost, err := env.EstimateCost(compiled, est) 1106 if err != nil { 1107 t.Fatalf("%v", err) 1108 } 1109 if estCost.Min != expectEsimatedCost.Min || estCost.Max != expectEsimatedCost.Max { 1110 t.Errorf("Expected estimated cost of %d..%d but got %d..%d", expectEsimatedCost.Min, expectEsimatedCost.Max, estCost.Min, estCost.Max) 1111 } 1112 prog, err := env.Program(compiled, cel.CostTracking(est)) 1113 if err != nil { 1114 t.Fatalf("%v", err) 1115 } 1116 _, details, err := prog.Eval(map[string]interface{}{"authorizer": NewAuthorizerVal(nil, alwaysAllowAuthorizer{})}) 1117 if err != nil { 1118 t.Fatalf("%v", err) 1119 } 1120 cost := details.ActualCost() 1121 if *cost != expectRuntimeCost { 1122 t.Errorf("Expected cost of %d but got %d", expectRuntimeCost, *cost) 1123 } 1124 } 1125 1126 func TestSize(t *testing.T) { 1127 exactSize := func(size int) checker.SizeEstimate { 1128 return checker.SizeEstimate{Min: uint64(size), Max: uint64(size)} 1129 } 1130 exactSizes := func(sizes ...int) []checker.SizeEstimate { 1131 results := make([]checker.SizeEstimate, len(sizes)) 1132 for i, size := range sizes { 1133 results[i] = exactSize(size) 1134 } 1135 return results 1136 } 1137 cases := []struct { 1138 name string 1139 function string 1140 overload string 1141 targetSize checker.SizeEstimate 1142 argSizes []checker.SizeEstimate 1143 expectSize checker.SizeEstimate 1144 }{ 1145 { 1146 name: "replace empty with char", 1147 function: "replace", 1148 targetSize: exactSize(3), // e.g. abc 1149 argSizes: exactSizes(0, 1), // e.g. replace "" with "_" 1150 expectSize: exactSize(7), // e.g. _a_b_c_ 1151 }, 1152 { 1153 name: "maybe replace char with empty", 1154 function: "replace", 1155 targetSize: exactSize(3), 1156 argSizes: exactSizes(1, 0), 1157 expectSize: checker.SizeEstimate{Min: 0, Max: 3}, 1158 }, 1159 { 1160 name: "maybe replace repeated", 1161 function: "replace", 1162 targetSize: exactSize(4), 1163 argSizes: exactSizes(2, 4), 1164 expectSize: checker.SizeEstimate{Min: 4, Max: 8}, 1165 }, 1166 { 1167 name: "maybe replace empty", 1168 function: "replace", 1169 targetSize: exactSize(4), 1170 argSizes: []checker.SizeEstimate{{Min: 0, Max: 1}, {Min: 0, Max: 2}}, 1171 expectSize: checker.SizeEstimate{Min: 0, Max: 14}, // len(__a__a__a__a__) == 14 1172 }, 1173 { 1174 name: "replace non-empty size range, maybe larger", 1175 function: "replace", 1176 targetSize: exactSize(4), 1177 argSizes: []checker.SizeEstimate{{Min: 1, Max: 1}, {Min: 1, Max: 2}}, 1178 expectSize: checker.SizeEstimate{Min: 4, Max: 8}, 1179 }, 1180 { 1181 name: "replace non-empty size range, maybe smaller", 1182 function: "replace", 1183 targetSize: exactSize(4), 1184 argSizes: []checker.SizeEstimate{{Min: 1, Max: 2}, {Min: 1, Max: 1}}, 1185 expectSize: checker.SizeEstimate{Min: 2, Max: 4}, 1186 }, 1187 } 1188 1189 originalPanicOnUnknown := panicOnUnknown 1190 panicOnUnknown = true 1191 t.Cleanup(func() { panicOnUnknown = originalPanicOnUnknown }) 1192 1193 est := &CostEstimator{SizeEstimator: &testCostEstimator{}} 1194 for _, tc := range cases { 1195 t.Run(tc.name, func(t *testing.T) { 1196 var targetNode checker.AstNode = testSizeNode{size: tc.targetSize} 1197 argNodes := make([]checker.AstNode, len(tc.argSizes)) 1198 for i, arg := range tc.argSizes { 1199 argNodes[i] = testSizeNode{size: arg} 1200 } 1201 result := est.EstimateCallCost(tc.function, tc.overload, &targetNode, argNodes) 1202 if result.ResultSize == nil { 1203 t.Fatalf("Expected ResultSize but got none") 1204 } 1205 if *result.ResultSize != tc.expectSize { 1206 t.Fatalf("Expected %+v but got %+v", tc.expectSize, *result.ResultSize) 1207 } 1208 }) 1209 } 1210 } 1211 1212 type testSizeNode struct { 1213 size checker.SizeEstimate 1214 } 1215 1216 var _ checker.AstNode = (*testSizeNode)(nil) 1217 1218 func (t testSizeNode) Path() []string { 1219 return nil // not needed 1220 } 1221 1222 func (t testSizeNode) Type() *types.Type { 1223 return nil // not needed 1224 } 1225 1226 func (t testSizeNode) Expr() ast.Expr { 1227 return nil // not needed 1228 } 1229 1230 func (t testSizeNode) ComputedSize() *checker.SizeEstimate { 1231 return &t.size 1232 } 1233 1234 type testCostEstimator struct { 1235 } 1236 1237 func (t *testCostEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstimate { 1238 expr, err := cel.TypeToExprType(element.Type()) 1239 if err != nil { 1240 return nil 1241 } 1242 switch expr.GetPrimitive() { 1243 case exprpb.Type_STRING: 1244 return &checker.SizeEstimate{Min: 0, Max: 12} 1245 case exprpb.Type_BYTES: 1246 return &checker.SizeEstimate{Min: 0, Max: 12} 1247 } 1248 return nil 1249 } 1250 1251 func (t *testCostEstimator) EstimateCallCost(function, overloadId string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { 1252 return nil 1253 } 1254 1255 type alwaysAllowAuthorizer struct{} 1256 1257 func (f alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { 1258 return authorizer.DecisionAllow, "", nil 1259 }