|
15 | 15 | package cel
|
16 | 16 |
|
17 | 17 | import (
|
| 18 | + "errors" |
18 | 19 | "fmt"
|
19 | 20 | "math"
|
20 | 21 | "reflect"
|
| 22 | + "strings" |
21 | 23 | "sync"
|
22 | 24 | "testing"
|
23 | 25 |
|
@@ -419,6 +421,307 @@ func TestEnvToConfig(t *testing.T) {
|
419 | 421 | }
|
420 | 422 | }
|
421 | 423 |
|
| 424 | +func TestEnvFromConfig(t *testing.T) { |
| 425 | + type exprCase struct { |
| 426 | + name string |
| 427 | + in any |
| 428 | + expr string |
| 429 | + iss error |
| 430 | + out ref.Val |
| 431 | + } |
| 432 | + tests := []struct { |
| 433 | + name string |
| 434 | + beforeOpts []EnvOption |
| 435 | + afterOpts []EnvOption |
| 436 | + conf *env.Config |
| 437 | + exprs []exprCase |
| 438 | + }{ |
| 439 | + { |
| 440 | + name: "std env", |
| 441 | + conf: env.NewConfig("std env"), |
| 442 | + exprs: []exprCase{ |
| 443 | + { |
| 444 | + name: "literal", |
| 445 | + expr: "'hello world'", |
| 446 | + out: types.String("hello world"), |
| 447 | + }, |
| 448 | + { |
| 449 | + name: "size", |
| 450 | + expr: "'hello world'.size()", |
| 451 | + out: types.Int(11), |
| 452 | + }, |
| 453 | + }, |
| 454 | + }, |
| 455 | + { |
| 456 | + name: "std env - imports", |
| 457 | + beforeOpts: []EnvOption{Types(&proto3pb.TestAllTypes{})}, |
| 458 | + conf: env.NewConfig("std env - context proto"). |
| 459 | + AddImports(env.NewImport("google.expr.proto3.test.TestAllTypes")), |
| 460 | + exprs: []exprCase{ |
| 461 | + { |
| 462 | + name: "literal", |
| 463 | + expr: "TestAllTypes{single_int64: 15}.single_int64", |
| 464 | + out: types.Int(15), |
| 465 | + }, |
| 466 | + }, |
| 467 | + }, |
| 468 | + { |
| 469 | + name: "std env - context proto", |
| 470 | + beforeOpts: []EnvOption{Types(&proto3pb.TestAllTypes{})}, |
| 471 | + conf: env.NewConfig("std env - context proto"). |
| 472 | + SetContainer("google.expr.proto3.test"). |
| 473 | + SetContextVariable(env.NewContextVariable("google.expr.proto3.test.TestAllTypes")), |
| 474 | + exprs: []exprCase{ |
| 475 | + { |
| 476 | + name: "field select literal", |
| 477 | + in: mustContextProto(t, &proto3pb.TestAllTypes{SingleInt64: 10}), |
| 478 | + expr: "TestAllTypes{single_int64: single_int64}.single_int64", |
| 479 | + out: types.Int(10), |
| 480 | + }, |
| 481 | + }, |
| 482 | + }, |
| 483 | + { |
| 484 | + name: "custom env - variables", |
| 485 | + beforeOpts: []EnvOption{Types(&proto3pb.TestAllTypes{})}, |
| 486 | + conf: env.NewConfig("custom env - variables"). |
| 487 | + SetStdLib(env.NewLibrarySubset().SetDisabled(true)). |
| 488 | + SetContainer("google.expr.proto3.test"). |
| 489 | + AddVariables(env.NewVariable("single_int64", env.NewTypeDesc("int"))), |
| 490 | + exprs: []exprCase{ |
| 491 | + { |
| 492 | + name: "field select literal", |
| 493 | + in: map[string]any{"single_int64": 42}, |
| 494 | + expr: "TestAllTypes{single_int64: single_int64}.single_int64", |
| 495 | + out: types.Int(42), |
| 496 | + }, |
| 497 | + { |
| 498 | + name: "invalid operator", |
| 499 | + in: map[string]any{"single_int64": 42}, |
| 500 | + expr: "TestAllTypes{single_int64: single_int64}.single_int64 + 1", |
| 501 | + iss: errors.New("undeclared reference"), |
| 502 | + }, |
| 503 | + }, |
| 504 | + }, |
| 505 | + { |
| 506 | + name: "custom env - functions", |
| 507 | + afterOpts: []EnvOption{ |
| 508 | + Function("plus", |
| 509 | + MemberOverload("int_plus_int", []*Type{IntType, IntType}, IntType, |
| 510 | + BinaryBinding(func(lhs, rhs ref.Val) ref.Val { |
| 511 | + l := lhs.(types.Int) |
| 512 | + r := rhs.(types.Int) |
| 513 | + return l + r |
| 514 | + }), |
| 515 | + ), |
| 516 | + )}, |
| 517 | + conf: env.NewConfig("custom env - functions"). |
| 518 | + SetStdLib(env.NewLibrarySubset().SetDisabled(true)). |
| 519 | + AddVariables(env.NewVariable("x", env.NewTypeDesc("int"))). |
| 520 | + AddFunctions(env.NewFunction("plus", |
| 521 | + env.NewMemberOverload("int_plus_int", |
| 522 | + env.NewTypeDesc("int"), |
| 523 | + []*env.TypeDesc{env.NewTypeDesc("int")}, |
| 524 | + env.NewTypeDesc("int"), |
| 525 | + ), |
| 526 | + )), |
| 527 | + exprs: []exprCase{ |
| 528 | + { |
| 529 | + name: "plus", |
| 530 | + in: map[string]any{"x": 42}, |
| 531 | + expr: "x.plus(2)", |
| 532 | + out: types.Int(44), |
| 533 | + }, |
| 534 | + { |
| 535 | + name: "plus invalid type", |
| 536 | + in: map[string]any{"x": 42}, |
| 537 | + expr: "x.plus(2.0)", |
| 538 | + iss: errors.New("no matching overload"), |
| 539 | + }, |
| 540 | + }, |
| 541 | + }, |
| 542 | + { |
| 543 | + name: "pure custom env", |
| 544 | + beforeOpts: []EnvOption{func(*Env) (*Env, error) { |
| 545 | + return NewCustomEnv() |
| 546 | + }}, |
| 547 | + conf: env.NewConfig("pure custom env").SetStdLib( |
| 548 | + env.NewLibrarySubset().AddIncludedFunctions([]*env.Function{{Name: "_==_"}}...), |
| 549 | + ), |
| 550 | + exprs: []exprCase{ |
| 551 | + { |
| 552 | + name: "equals", |
| 553 | + expr: "'hello world' == 'hello'", |
| 554 | + out: types.False, |
| 555 | + }, |
| 556 | + { |
| 557 | + name: "not equals - invalid", |
| 558 | + expr: "'hello world' != 'hello'", |
| 559 | + iss: errors.New("undeclared reference"), |
| 560 | + }, |
| 561 | + }, |
| 562 | + }, |
| 563 | + { |
| 564 | + name: "std env - allow subset", |
| 565 | + conf: env.NewConfig("std env - allow subset").SetStdLib( |
| 566 | + env.NewLibrarySubset().AddIncludedFunctions([]*env.Function{{Name: "_==_"}}...), |
| 567 | + ), |
| 568 | + exprs: []exprCase{ |
| 569 | + { |
| 570 | + name: "equals", |
| 571 | + expr: "'hello world' == 'hello'", |
| 572 | + out: types.False, |
| 573 | + }, |
| 574 | + { |
| 575 | + name: "not equals - invalid", |
| 576 | + expr: "'hello world' != 'hello'", |
| 577 | + iss: errors.New("undeclared reference"), |
| 578 | + }, |
| 579 | + }, |
| 580 | + }, |
| 581 | + { |
| 582 | + name: "std env - deny subset", |
| 583 | + conf: env.NewConfig("std env - deny subset").SetStdLib( |
| 584 | + env.NewLibrarySubset().AddExcludedFunctions([]*env.Function{{Name: "size"}}...), |
| 585 | + ), |
| 586 | + exprs: []exprCase{ |
| 587 | + { |
| 588 | + name: "size - invalid", |
| 589 | + expr: "'hello world'.size()", |
| 590 | + iss: errors.New("undeclared reference"), |
| 591 | + }, |
| 592 | + { |
| 593 | + name: "equals", |
| 594 | + expr: "'hello world' == 'hello'", |
| 595 | + out: types.False, |
| 596 | + }, |
| 597 | + }, |
| 598 | + }, |
| 599 | + { |
| 600 | + name: "extensions", |
| 601 | + conf: env.NewConfig("extensions"). |
| 602 | + AddVariables( |
| 603 | + env.NewVariable("m", |
| 604 | + env.NewTypeDesc("map", env.NewTypeDesc("string"), env.NewTypeDesc("string")))). |
| 605 | + AddExtensions(env.NewExtension("optional", math.MaxUint32)), |
| 606 | + exprs: []exprCase{ |
| 607 | + { |
| 608 | + name: "optional none", |
| 609 | + expr: "optional.none()", |
| 610 | + out: types.OptionalNone, |
| 611 | + }, |
| 612 | + { |
| 613 | + name: "optional key", |
| 614 | + expr: "m.?key.hasValue()", |
| 615 | + in: map[string]any{"m": map[string]string{"key": "value"}}, |
| 616 | + out: types.True, |
| 617 | + }, |
| 618 | + }, |
| 619 | + }, |
| 620 | + } |
| 621 | + for _, tst := range tests { |
| 622 | + tc := tst |
| 623 | + t.Run(tc.name, func(t *testing.T) { |
| 624 | + opts := tc.beforeOpts |
| 625 | + opts = append(opts, FromConfig(tc.conf, func(elem any) (EnvOption, bool) { |
| 626 | + if ext, ok := elem.(*env.Extension); ok && ext.Name == "optional" { |
| 627 | + ver, _ := ext.GetVersion() |
| 628 | + return OptionalTypes(OptionalTypesVersion(ver)), true |
| 629 | + } |
| 630 | + return nil, false |
| 631 | + })) |
| 632 | + opts = append(opts, tc.afterOpts...) |
| 633 | + var e *Env |
| 634 | + var err error |
| 635 | + if tc.conf.StdLib != nil { |
| 636 | + e, err = NewCustomEnv(opts...) |
| 637 | + } else { |
| 638 | + e, err = NewEnv(opts...) |
| 639 | + } |
| 640 | + if err != nil { |
| 641 | + t.Fatalf("NewEnv(FromConfig()) failed: %v", err) |
| 642 | + } |
| 643 | + for _, ex := range tc.exprs { |
| 644 | + t.Run(ex.name, func(t *testing.T) { |
| 645 | + ast, iss := e.Compile(ex.expr) |
| 646 | + if iss.Err() != nil { |
| 647 | + if ex.iss == nil || !strings.Contains(iss.Err().Error(), ex.iss.Error()) { |
| 648 | + t.Errorf("e.Compile() failed with %v, wanted %v", iss.Err(), ex.iss) |
| 649 | + } |
| 650 | + return |
| 651 | + } |
| 652 | + if ex.iss != nil { |
| 653 | + t.Fatalf("e.Compile() succeeded, wanted error %v", ex.iss) |
| 654 | + } |
| 655 | + prg, err := e.Program(ast) |
| 656 | + if err != nil { |
| 657 | + t.Fatalf("e.Program() failed: %v", err) |
| 658 | + } |
| 659 | + var in any = map[string]any{} |
| 660 | + if ex.in != nil { |
| 661 | + in = ex.in |
| 662 | + } |
| 663 | + out, _, err := prg.Eval(in) |
| 664 | + if err != nil { |
| 665 | + t.Fatalf("prg.Eval() failed: %v", err) |
| 666 | + } |
| 667 | + if out.Equal(ex.out) != types.True { |
| 668 | + t.Errorf("prg.Eval() got %v, wanted %v", out, ex.out) |
| 669 | + } |
| 670 | + }) |
| 671 | + } |
| 672 | + }) |
| 673 | + } |
| 674 | +} |
| 675 | + |
| 676 | +func TestEnvFromConfigErrors(t *testing.T) { |
| 677 | + tests := []struct { |
| 678 | + name string |
| 679 | + conf *env.Config |
| 680 | + want error |
| 681 | + }{ |
| 682 | + { |
| 683 | + name: "invalid subset", |
| 684 | + conf: env.NewConfig("invalid subset").SetStdLib(env.NewLibrarySubset().SetDisableMacros(true)), |
| 685 | + want: errors.New("invalid subset"), |
| 686 | + }, |
| 687 | + { |
| 688 | + name: "invalid import", |
| 689 | + conf: env.NewConfig("invalid import").AddImports(env.NewImport("")), |
| 690 | + want: errors.New("invalid import"), |
| 691 | + }, |
| 692 | + { |
| 693 | + name: "invalid context proto", |
| 694 | + conf: env.NewConfig("invalid context proto").SetContextVariable(env.NewContextVariable("invalid")), |
| 695 | + want: errors.New("invalid context proto type"), |
| 696 | + }, |
| 697 | + { |
| 698 | + name: "undefined variable type", |
| 699 | + conf: env.NewConfig("undefined variable type").AddVariables(env.NewVariable("undef", env.NewTypeDesc("undefined"))), |
| 700 | + want: errors.New("invalid variable"), |
| 701 | + }, |
| 702 | + { |
| 703 | + name: "undefined function type", |
| 704 | + conf: env.NewConfig("undefined function type").AddFunctions(env.NewFunction("invalid", env.NewOverload("invalid", []*env.TypeDesc{}, env.NewTypeDesc("undefined")))), |
| 705 | + want: errors.New("invalid function"), |
| 706 | + }, |
| 707 | + { |
| 708 | + name: "unrecognized extension", |
| 709 | + conf: env.NewConfig("unrecognized extension"). |
| 710 | + AddExtensions(env.NewExtension("optional", math.MaxUint32)), |
| 711 | + want: errors.New("unrecognized extension"), |
| 712 | + }, |
| 713 | + } |
| 714 | + for _, tst := range tests { |
| 715 | + tc := tst |
| 716 | + t.Run(tc.name, func(t *testing.T) { |
| 717 | + _, err := NewEnv(FromConfig(tc.conf)) |
| 718 | + if err == nil || !strings.Contains(err.Error(), tc.want.Error()) { |
| 719 | + t.Fatalf("NewEnv(FromConfig()) got %v, wanted error containing %v", err, tc.want) |
| 720 | + } |
| 721 | + }) |
| 722 | + } |
| 723 | +} |
| 724 | + |
422 | 725 | func BenchmarkNewCustomEnvLazy(b *testing.B) {
|
423 | 726 | b.ResetTimer()
|
424 | 727 | for i := 0; i < b.N; i++ {
|
|
0 commit comments