k8s.io/apiserver@v0.31.1/pkg/cel/lazy/lazy_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 lazy
    18  
    19  import (
    20  	"fmt"
    21  	"testing"
    22  
    23  	"github.com/google/cel-go/cel"
    24  	"github.com/google/cel-go/common/types"
    25  	"github.com/google/cel-go/common/types/ref"
    26  	"github.com/google/cel-go/interpreter"
    27  
    28  	"k8s.io/apimachinery/pkg/util/version"
    29  	apiservercel "k8s.io/apiserver/pkg/cel"
    30  	"k8s.io/apiserver/pkg/cel/environment"
    31  )
    32  
    33  func TestLazyMapType(t *testing.T) {
    34  	env, variablesType, err := buildTestEnv()
    35  	if err != nil {
    36  		t.Fatal(err)
    37  	}
    38  	variablesMap := NewMapValue(variablesType)
    39  	activation := &testActivation{variables: variablesMap}
    40  
    41  	// add foo as a string
    42  	variablesType.Fields["foo"] = apiservercel.NewDeclField("foo", apiservercel.StringType, true, nil, nil)
    43  	variablesMap.Append("foo", func(_ *MapValue) ref.Val {
    44  		return types.String("foo-string")
    45  	})
    46  
    47  	exp := "variables.foo == 'foo-string'"
    48  	v, err := compileAndRun(env, activation, exp)
    49  	if err != nil {
    50  		t.Fatalf("%q: %v", exp, err)
    51  	}
    52  	if !v.Value().(bool) {
    53  		t.Errorf("expected equal but got non-equal")
    54  	}
    55  
    56  	evalCounter := 0
    57  	// add dict as a map constructed from an expression
    58  	variablesType.Fields["dict"] = apiservercel.NewDeclField("dict", apiservercel.DynType, true, nil, nil)
    59  	variablesMap.Append("dict", func(_ *MapValue) ref.Val {
    60  		evalCounter++
    61  		v, err := compileAndRun(env, activation, `{"a": "a"}`)
    62  		if err != nil {
    63  			return types.NewErr(err.Error())
    64  		}
    65  		return v
    66  	})
    67  
    68  	// iterate the map with .all
    69  	exp = `variables.all(n, n != "")`
    70  	v, err = compileAndRun(env, activation, exp)
    71  	if err != nil {
    72  		t.Fatalf("%q: %v", exp, err)
    73  	}
    74  	if v.Value().(bool) != true {
    75  		t.Errorf("%q: wrong result: %v", exp, v.Value())
    76  	}
    77  
    78  	// add unused as a string
    79  	variablesType.Fields["unused"] = apiservercel.NewDeclField("unused", apiservercel.StringType, true, nil, nil)
    80  	variablesMap.Append("unused", func(_ *MapValue) ref.Val {
    81  		t.Fatalf("unused variable must not be evaluated")
    82  		return nil
    83  	})
    84  
    85  	exp = "variables.dict.a + ' ' + variables.dict.a + ' ' + variables.foo"
    86  	v, err = compileAndRun(env, activation, exp)
    87  	if err != nil {
    88  		t.Fatalf("%q: %v", exp, err)
    89  	}
    90  	if v.Value().(string) != "a a foo-string" {
    91  		t.Errorf("%q: wrong result: %v", exp, v.Value())
    92  	}
    93  	if evalCounter != 1 {
    94  		t.Errorf("expected eval %d times but got %d", 1, evalCounter)
    95  	}
    96  
    97  	// unused due to boolean short-circuiting
    98  	// if `variables.unused` is evaluated, the whole test will have a fatal error and exit.
    99  	exp = "variables.dict.a == 'wrong' && variables.unused == 'unused'"
   100  	v, err = compileAndRun(env, activation, exp)
   101  	if err != nil {
   102  		t.Fatalf("%q: %v", exp, err)
   103  	}
   104  	if v.Value().(bool) != false {
   105  		t.Errorf("%q: wrong result: %v", exp, v.Value())
   106  	}
   107  }
   108  
   109  type testActivation struct {
   110  	variables *MapValue
   111  }
   112  
   113  func compileAndRun(env *cel.Env, activation *testActivation, exp string) (ref.Val, error) {
   114  	ast, issues := env.Compile(exp)
   115  	if issues != nil {
   116  		return nil, fmt.Errorf("fail to compile: %v", issues)
   117  	}
   118  	prog, err := env.Program(ast)
   119  	if err != nil {
   120  		return nil, fmt.Errorf("cannot create program: %w", err)
   121  	}
   122  	v, _, err := prog.Eval(activation)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("cannot eval program: %w", err)
   125  	}
   126  	return v, nil
   127  }
   128  
   129  func buildTestEnv() (*cel.Env, *apiservercel.DeclType, error) {
   130  	variablesType := apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, 0)
   131  	variablesType.Fields = make(map[string]*apiservercel.DeclField)
   132  	envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true).Extend(
   133  		environment.VersionedOptions{
   134  			IntroducedVersion: version.MajorMinor(1, 28),
   135  			EnvOptions: []cel.EnvOption{
   136  				cel.Variable("variables", variablesType.CelType()),
   137  			},
   138  			DeclTypes: []*apiservercel.DeclType{
   139  				variablesType,
   140  			},
   141  		})
   142  	if err != nil {
   143  		return nil, nil, err
   144  	}
   145  	// TODO: change to NewExpressions after 1.28
   146  	env, err := envSet.Env(environment.StoredExpressions)
   147  	return env, variablesType, err
   148  }
   149  
   150  func (a *testActivation) ResolveName(name string) (any, bool) {
   151  	switch name {
   152  	case "variables":
   153  		return a.variables, true
   154  	default:
   155  		return nil, false
   156  	}
   157  }
   158  
   159  func (a *testActivation) Parent() interpreter.Activation {
   160  	return nil
   161  }