Run Ginkgo automation tests on LambdaTest cloud grid
Perform automation testing on 3000+ real desktop and mobile devices online.
package expressions
import (
"fmt"
"strings"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/overloads"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter"
"github.com/google/cel-go/interpreter/functions"
)
// unknownDecorator returns a decorator for inspecting and handling unknowns at runtime. This
// decorator is called before _any_ attribute or function is evaluated in CEL, allowing us to
// intercept and return our own values.
//
// For example, natively in CEL `size(null)` returns a "no such overload" error. We intercept
// the evalutation of `size(null)` and return a new type (0) instead of the error.
func unknownDecorator(act interpreter.PartialActivation) interpreter.InterpretableDecorator {
// Create a new dispatcher with all functions added
dispatcher := interpreter.NewDispatcher()
overloads := append(functions.StandardOverloads(), celOverloads()...)
_ = dispatcher.Add(overloads...)
return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
// If this is a fold call, this is a macro (exists, has, etc), and is not an InterpretableCall
call, ok := i.(interpreter.InterpretableCall)
if !ok {
return i, nil
}
argTypes := &argColl{}
args := call.Args()
for _, arg := range args {
// We want both attributes (variables) & consts to check for coercion.
argTypes.Add(arg.Eval(act))
}
if argTypes.TypeLen() == 1 && !argTypes.Exists(types.ErrType) && !argTypes.Exists(types.UnknownType) {
// A single type used within the function with no error and unknown is
// safe to call as usual.
return i, nil
}
if argTypes.Exists(types.ErrType) || argTypes.Exists(types.UnknownType) {
// We work with unknown and error types, handling both as non-existent
// types.
//
// Errors can appear when calling macros on unknowns (eg:
// event.data.nonexistent.subkey.contains("something")).
//
// This could be because:
//
// 1. we're calling a macro on an unknown value. This happens before we can intercept
// the InterpretableCall and will always happen. That's fine, and this produces the
// error "no such key". These are the errors we want to intercept.
//
// 2. we're inside a macro and we're using the __result__ or lambda
// variable. This error contains "no such attribute", and this is a usual
// part of runing macros. XXX: Figure out why Eval() on macro variables fails:
// this is actually _not_ an error.
for _, val := range argTypes.OfType(types.ErrType) {
if strings.HasPrefix(val.(error).Error(), "no such attribute") {
// This must be a macro call; handle as usual
return i, nil
}
}
// This is an unknown type. Dependent on the function being called return
// a concrete true or false value by default.
return handleUnknownCall(call, argTypes)
}
// Here we have multiple types called together. If these are coercible, we'll
// attempt to coerce them (eg. ints and floats).
//
// We can't create a custom null type, because Compare may run on String, Int, Double,
// etc: we'd have to wrap every type and add null checking. This is a maintenance
// en and could be bug-prone.
//
// Therefore, we have to evaluate this here within a decorator.
if argTypes.Exists(types.NullType) && argTypes.ArgLen() == 2 {
switch call.Function() {
case operators.Equals:
return staticCall{result: types.False, InterpretableCall: call}, nil
case operators.NotEquals:
return staticCall{result: types.True, InterpretableCall: call}, nil
}
// Other operators, such as >, <=, depend on the argument order to evaluate
// correctly.
//
// We must create a new zero type in place of the null argument,
// then fetch the overload from the standard dispatcher and run the function.
args, err := argTypes.ZeroValArgs()
if err != nil {
return i, nil
}
fn, ok := dispatcher.FindOverload(call.Function())
if !ok {
return i, nil
}
return staticCall{result: fn.Binary(args[0], args[1]), InterpretableCall: call}, nil
}
return i, nil
}
}
// By default, CEL tracks unknowns as a separate value. This is fantastic, but when
// we're evaluating expressions we want to treat unknowns as nulls.
//
// This functionality adds custom logic for each overload to return a static ref.Val
// which is used in place of unknown.
func handleUnknownCall(i interpreter.InterpretableCall, args *argColl) (interpreter.Interpretable, error) {
switch i.Function() {
case operators.Equals:
// Comparing an unknown to null is true, else return false.
result := types.False
if args.Exists(types.NullType) {
result = types.True
}
return staticCall{result: result, InterpretableCall: i}, nil
case operators.NotEquals:
if args.Exists(types.NullType) {
// Unknowns are null, so this is false.
return staticCall{result: types.False, InterpretableCall: i}, nil
}
// Are we comparing against a zero type (eg. empty string, 0).
// The only item that should return true is not equals, as nil is always not equals
return staticCall{result: types.True, InterpretableCall: i}, nil
case operators.Less, operators.LessEquals:
// Unknown is less than anything.
if args.arguments[0].Type() == types.UnknownType || args.arguments[0].Type() == types.ErrType {
return staticCall{result: types.True, InterpretableCall: i}, nil
}
return staticCall{result: types.False, InterpretableCall: i}, nil
case operators.Greater, operators.GreaterEquals:
// If the first arg is unkown, return false: unknown is not greater.
if args.arguments[0].Type() == types.UnknownType || args.arguments[0].Type() == types.ErrType {
return staticCall{result: types.False, InterpretableCall: i}, nil
}
return staticCall{result: types.True, InterpretableCall: i}, nil
case overloads.Size,
overloads.SizeString,
overloads.SizeBytes,
overloads.SizeList,
overloads.SizeStringInst,
overloads.SizeBytesInst,
overloads.SizeListInst,
overloads.SizeMapInst:
// Size on unknowns should always return zero to avoid type errors.
return staticCall{result: types.IntZero, InterpretableCall: i}, nil
default:
// By default, return false, for eaxmple: "_<_", "@in", "@not_strictly_false"
// return staticCall{result: types.False, InterpretableCall: call}, nil
return staticCall{result: types.False, InterpretableCall: i}, nil
}
}
// staticCall represents a wrapped interpreter.InterpretableCall function within
// an expression that always returns a static value.
type staticCall struct {
interpreter.InterpretableCall
result ref.Val
}
func (u staticCall) Eval(ctx interpreter.Activation) ref.Val {
return u.result
}
// argColl inspects all of the types available within a function call in
// CEL, storing their type information.
type argColl struct {
// types represents a map of types to their values used within the
// function.
types map[ref.Type][]ref.Val
// arguments represents the function arguments, in order.
arguments []ref.Val
}
// Add adds a new value to the type collection, storing its type in the map.
func (t *argColl) Add(vals ...ref.Val) {
if t.types == nil {
t.types = map[ref.Type][]ref.Val{}
}
for _, val := range vals {
// Store the arguments in order (left and right hand side of operators)
t.arguments = append(t.arguments, val)
typ := val.Type()
coll, ok := t.types[typ]
if !ok {
t.types[typ] = []ref.Val{val}
return
}
t.types[typ] = append(coll, val)
}
}
func (t *argColl) TypeLen() int {
return len(t.types)
}
func (t *argColl) ArgLen() int {
return len(t.arguments)
}
func (t *argColl) Exists(typ ref.Type) bool {
_, ok := t.types[typ]
return ok
}
// OfType returns all arguments of the given type.
func (t *argColl) OfType(typ ref.Type) []ref.Val {
coll, ok := t.types[typ]
if !ok {
return nil
}
return coll
}
// NonNull returns all non-null types as a slice.
func (t *argColl) NonNull() []ref.Type {
coll := []ref.Type{}
for typ := range t.types {
if typ == types.NullType {
continue
}
coll = append(coll, typ)
}
return coll
}
// ZeroValArgs returns all args with null types replaced as zero values
func (t *argColl) ZeroValArgs() ([]ref.Val, error) {
typ := t.NonNull()
if len(typ) != 1 {
return t.arguments, fmt.Errorf("not exactly one other non-null type present")
}
coll := make([]ref.Val, len(t.arguments))
for n, arg := range t.arguments {
coll[n] = arg
if arg.Type() == types.NullType {
coll[n] = zeroVal(typ[0])
}
}
return coll, nil
}
// zeroVal returns a zero value for common cel datatypes. This helps us
// convert null values to a zero value of a specific type.
func zeroVal(t ref.Type) ref.Val {
switch t.TypeName() {
case "int":
return types.IntZero
case "uint":
return types.Uint(0)
case "double":
return types.Double(0)
case "string":
return types.String("")
}
return types.NullValue
}
// Package expressions provides the ability to inspect and evaluate arbitrary
// user-defined expressions. We use the Cel-Go package as a runtime to implement
// computationally bounded, non-turing complete expressions with a familiar c-like
// syntax.
//
// Unlike cel-go's defaults, this package handles unknowns similarly to null values,
// and allows arbitrary attributes within expressions without errors. We also
// provide basic type coercion allowing eg. int <> float comparisons, which errors
// within cel by default.
//
// Expressions can be inspected to determine the variables that they reference,
// partially evaluated with missing data, and report timestamps used within the
// expression for future reference (eg. recomputing state at that time).
package expressions
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/pkg/errors"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
var (
ErrNoResult = errors.New("expression did not return true or false")
ErrInvalidResult = errors.New("expression errored")
)
// Evaluable represents a cacheable, goroutine safe manager for evaluating a single
// precompiled expression with arbitrary data.
type Evaluable interface {
// Evaluate tests the incoming Data against the expression that is
// stored within the Evaluable implementation.
//
// Attributes that are present within the expression but missing from the
// data should be treated as null values; the expression must not error.
Evaluate(ctx context.Context, data *Data) (bool, *time.Time, error)
// UsedAttributes returns the attributes that are referenced within the
// expression.
UsedAttributes(ctx context.Context) *UsedAttributes
// FilteredAttributes filters the given data to contain only attributes
// referenced from the expression.
FilteredAttributes(ctx context.Context, data *Data) *Data
}
// Evaluate is a helper function to create a new, cached expression evaluator to evaluate
// the given data immediately.
func Evaluate(ctx context.Context, expression string, input map[string]interface{}) (bool, *time.Time, error) {
eval, err := NewExpressionEvaluator(ctx, expression)
if err != nil {
return false, nil, err
}
data := NewData(input)
return eval.Evaluate(ctx, data)
}
// NewExpressionEvaluator returns a new Evaluable instance for a given expression. The
// instance can be used across many goroutines to evaluate the expression against any
// data. The Evaluable instance is loaded from the cache, or is cached if not found.
func NewExpressionEvaluator(ctx context.Context, expression string) (Evaluable, error) {
e, err := env()
if err != nil {
return nil, err
}
ast, issues := e.Compile(expression)
if issues != nil {
return nil, fmt.Errorf("error compiling expression: %w", issues.Err())
}
eval := &expressionEvaluator{
ast: ast,
env: e,
expression: expression,
}
if err := eval.parseAttributes(ctx); err != nil {
return nil, err
}
return eval, nil
}
type expressionEvaluator struct {
// TODO: Refactor unknownEval to remove the need tor attr.Eval(activation),
// and make dateRefs thread safe. We can then place a cel.Program on the
// evaluator as it's thread safe. Without these changes, Programs are bound
// to specific expression & data combinations.
ast *cel.Ast
env *cel.Env
// expression is the raw expression
expression string
// attrs is allows us to determine which attributes are used within an expression.
// This is needed to create partial activations, and is also used to optimistically
// load only necessary attributes.
attrs *UsedAttributes
}
// Evaluate compiles an expression string against a set of variables, returning whether the
// expression evaluates to true, the next earliest time to re-test the evaluation (if dates are
// compared), and any errors.
func (e *expressionEvaluator) Evaluate(ctx context.Context, data *Data) (bool, *time.Time, error) {
if data == nil {
return false, nil, nil
}
act, err := data.Partial(ctx, *e.attrs)
if err != nil {
return false, nil, err
}
// We want to perform an exhaustive search and track the state of the search
// to see if dates are compared, then return the minimum date compared.
tr, td := timeDecorator(act)
// Create the program, refusing to short circuit if a match is found.
//
// This will add all functions from functions.StandardOverloads as we
// created the environment with our custom library.
program, err := e.env.Program(
e.ast,
cel.EvalOptions(cel.OptExhaustiveEval, cel.OptTrackState, cel.OptPartialEval), // Exhaustive, always, right now.
cel.CustomDecorator(unknownDecorator(act)),
cel.CustomDecorator(td),
)
if err != nil {
return false, nil, err
}
result, _, err := program.Eval(act)
if result == nil {
return false, nil, ErrNoResult
}
if types.IsUnknown(result) {
// When evaluating to a strict result this should never happen. We inject a decorator
// to handle unknowns as values similar to null, and should always get a value.
return false, nil, nil
}
if types.IsError(result) {
return false, nil, errors.Wrapf(ErrInvalidResult, "invalid type comparison: %s", err.Error())
}
if err != nil {
// This shouldn't be handled, as we should get an Error type in result above.
return false, nil, fmt.Errorf("error evaluating expression '%s': %w", e.expression, err)
}
b, ok := result.Value().(bool)
if !ok {
return false, nil, errors.Wrapf(ErrInvalidResult, "returned type %T (%s)", result, result)
}
// Find earliest date that we need to test against.
earliest := tr.Next()
return b, earliest, nil
}
// UsedAttributes returns the attributes used within the expression.
func (e *expressionEvaluator) UsedAttributes(ctx context.Context) *UsedAttributes {
return e.attrs
}
// FilteredAttributes returns a new Data pointer with only the attributes
// used within the expression.
func (e *expressionEvaluator) FilteredAttributes(ctx context.Context, d *Data) *Data {
if d == nil {
return nil
}
filtered := map[string]interface{}{}
current := filtered
stack := e.attrs.FullPaths()
for len(stack) > 0 {
path := stack[0]
stack = stack[1:]
val, ok := d.Get(ctx, path)
if !ok {
continue
}
for n, part := range path {
if n == len(path)-1 {
// This is the value.
current[part] = val
continue
}
if _, ok := current[part]; !ok {
current[part] = map[string]interface{}{}
}
current = current[part].(map[string]interface{})
}
current = filtered
}
// It is safe to set data directly here, as we've manually
// created a map containing raw values from a previously set
// Data field. This prevents us from needlesly mapifying
// data from a constructor.
return &Data{data: filtered}
}
// ParseAttributes returns the attributes used within the expression.
func (e *expressionEvaluator) parseAttributes(ctx context.Context) error {
if e.attrs != nil {
return nil
}
attrs := &UsedAttributes{
Root: []string{},
Fields: map[string][][]string{},
}
// Walk through the AST, looking for all instances of "select_expr" expression
// kinds. These elements are specifically selecting fields from parents, which
// is exactly what we need to figure out the variables used within an expression.
stack := []*expr.Expr{e.ast.Expr()}
for len(stack) > 0 {
ast := stack[0]
stack = stack[1:]
// Depending on the item, add the following
switch ast.ExprKind.(type) {
case *expr.Expr_ComprehensionExpr:
// eg. "event.data.tags.exists(x, x == 'Open'), so put what we're iterating over
// onto the stack to parse, ignoring this function call but adding the data.
c := ast.GetComprehensionExpr()
stack = append(stack, c.IterRange)
case *expr.Expr_CallExpr:
// Everything is a function call:
// - > evaluates to _>_ with two arguments, etc.
// This means pop all args onto the stack so that we can find
// all select expressions.
stack = append(stack, ast.GetCallExpr().GetArgs()...)
case *expr.Expr_IdentExpr:
name := ast.GetIdentExpr().Name
attrs.add(name, nil)
case *expr.Expr_SelectExpr:
// Note that the select expression unravels from the deepest key first:
// given "event.data.foo.bar", the current ast node will be for "foo"
// and the field name will be for "bar".
//
// Iterate through all object selects until there are no more, adding
// to the path.
path := []string{}
for ast.GetSelectExpr() != nil {
path = append([]string{ast.GetSelectExpr().Field}, path...)
ast = ast.GetSelectExpr().Operand
}
ident := ast.GetIdentExpr()
caller := ast.GetCallExpr()
if ident == nil && caller != nil && caller.Function == "_[_]" {
// This might be square notation: "actions[1]". This should
// have two args: the object (eg. actions), which is an
// IdentExpr, and a ConstExpr containing the number.
args := caller.GetArgs()
if len(args) != 2 {
return fmt.Errorf("unknown number of callers for bracket notation: %d", len(args))
}
// Functions have been rewritten to move "actions.1" into a string:
// actions["1"]
id := args[1].GetConstExpr().GetStringValue()
path = append([]string{args[0].GetIdentExpr().GetName(), id}, path...)
}
if ident != nil {
path = append([]string{ident.Name}, path...)
}
root := path[0]
fields := path[1:]
attrs.add(root, fields)
}
}
e.attrs = attrs
return nil
}
// UsedAttributes represents the evaluated expression's root and top-level fields used.
type UsedAttributes struct {
// Root represents root-level variables used within the expression
Root []string
// Fields represent fields within each root-level variable accessed.
//
// For example, given an attribute of "event.data.index", this map holds
// a key of "event" with a slice of [][]string{{"data", "index"}}
Fields map[string][][]string
// exists
exists map[string]struct{}
}
// FullPaths returns a slice of path slices with the roots appended.
func (u UsedAttributes) FullPaths() [][]string {
paths := [][]string{}
for root, items := range u.Fields {
for _, path := range items {
path = append([]string{root}, path...)
paths = append(paths, path)
}
}
return paths
}
func (u *UsedAttributes) add(root string, path []string) {
if u.exists == nil {
u.exists = map[string]struct{}{}
}
if _, ok := u.Fields[root]; !ok {
u.Root = append(u.Root, root)
u.Fields[root] = [][]string{}
}
// Add this once.
key := fmt.Sprintf("%s.%s", root, strings.Join(path, "."))
if _, ok := u.exists[key]; !ok && len(path) > 0 {
u.Fields[root] = append(u.Fields[root], path)
// store this key so it's not duplicated.
u.exists[key] = struct{}{}
}
}
package internal
import (
"fmt"
"reflect"
"sort"
"sync"
"github.com/onsi/ginkgo/v2/types"
)
var _global_node_id_counter = uint(0)
var _global_id_mutex = &sync.Mutex{}
func UniqueNodeID() uint {
//There's a reace in the internal integration tests if we don't make
//accessing _global_node_id_counter safe across goroutines.
_global_id_mutex.Lock()
defer _global_id_mutex.Unlock()
_global_node_id_counter += 1
return _global_node_id_counter
}
type Node struct {
ID uint
NodeType types.NodeType
Text string
Body func()
CodeLocation types.CodeLocation
NestingLevel int
SynchronizedBeforeSuiteProc1Body func() []byte
SynchronizedBeforeSuiteAllProcsBody func([]byte)
SynchronizedAfterSuiteAllProcsBody func()
SynchronizedAfterSuiteProc1Body func()
ReportEachBody func(types.SpecReport)
ReportAfterSuiteBody func(types.Report)
MarkedFocus bool
MarkedPending bool
MarkedSerial bool
MarkedOrdered bool
MarkedOncePerOrdered bool
FlakeAttempts int
Labels Labels
NodeIDWhereCleanupWasGenerated uint
}
// Decoration Types
type focusType bool
type pendingType bool
type serialType bool
type orderedType bool
type honorsOrderedType bool
const Focus = focusType(true)
const Pending = pendingType(true)
const Serial = serialType(true)
const Ordered = orderedType(true)
const OncePerOrdered = honorsOrderedType(true)
type FlakeAttempts uint
type Offset uint
type Done chan<- interface{} // Deprecated Done Channel for asynchronous testing
type Labels []string
func UnionOfLabels(labels ...Labels) Labels {
out := Labels{}
seen := map[string]bool{}
for _, labelSet := range labels {
for _, label := range labelSet {
if !seen[label] {
seen[label] = true
out = append(out, label)
}
}
}
return out
}
func PartitionDecorations(args ...interface{}) ([]interface{}, []interface{}) {
decorations := []interface{}{}
remainingArgs := []interface{}{}
for _, arg := range args {
if isDecoration(arg) {
decorations = append(decorations, arg)
} else {
remainingArgs = append(remainingArgs, arg)
}
}
return decorations, remainingArgs
}
func isDecoration(arg interface{}) bool {
switch t := reflect.TypeOf(arg); {
case t == nil:
return false
case t == reflect.TypeOf(Offset(0)):
return true
case t == reflect.TypeOf(types.CodeLocation{}):
return true
case t == reflect.TypeOf(Focus):
return true
case t == reflect.TypeOf(Pending):
return true
case t == reflect.TypeOf(Serial):
return true
case t == reflect.TypeOf(Ordered):
return true
case t == reflect.TypeOf(OncePerOrdered):
return true
case t == reflect.TypeOf(FlakeAttempts(0)):
return true
case t == reflect.TypeOf(Labels{}):
return true
case t.Kind() == reflect.Slice && isSliceOfDecorations(arg):
return true
default:
return false
}
}
func isSliceOfDecorations(slice interface{}) bool {
vSlice := reflect.ValueOf(slice)
if vSlice.Len() == 0 {
return false
}
for i := 0; i < vSlice.Len(); i++ {
if !isDecoration(vSlice.Index(i).Interface()) {
return false
}
}
return true
}
func NewNode(deprecationTracker *types.DeprecationTracker, nodeType types.NodeType, text string, args ...interface{}) (Node, []error) {
baseOffset := 2
node := Node{
ID: UniqueNodeID(),
NodeType: nodeType,
Text: text,
Labels: Labels{},
CodeLocation: types.NewCodeLocation(baseOffset),
NestingLevel: -1,
}
errors := []error{}
appendError := func(err error) {
if err != nil {
errors = append(errors, err)
}
}
args = unrollInterfaceSlice(args)
remainingArgs := []interface{}{}
//First get the CodeLocation up-to-date
for _, arg := range args {
switch v := arg.(type) {
case Offset:
node.CodeLocation = types.NewCodeLocation(baseOffset + int(v))
case types.CodeLocation:
node.CodeLocation = v
default:
remainingArgs = append(remainingArgs, arg)
}
}
labelsSeen := map[string]bool{}
trackedFunctionError := false
args = remainingArgs
remainingArgs = []interface{}{}
//now process the rest of the args
for _, arg := range args {
switch t := reflect.TypeOf(arg); {
case t == reflect.TypeOf(float64(0)):
break //ignore deprecated timeouts
case t == reflect.TypeOf(Focus):
node.MarkedFocus = bool(arg.(focusType))
if !nodeType.Is(types.NodeTypesForContainerAndIt) {
appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "Focus"))
}
case t == reflect.TypeOf(Pending):
node.MarkedPending = bool(arg.(pendingType))
if !nodeType.Is(types.NodeTypesForContainerAndIt) {
appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "Pending"))
}
case t == reflect.TypeOf(Serial):
node.MarkedSerial = bool(arg.(serialType))
if !nodeType.Is(types.NodeTypesForContainerAndIt) {
appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "Serial"))
}
case t == reflect.TypeOf(Ordered):
node.MarkedOrdered = bool(arg.(orderedType))
if !nodeType.Is(types.NodeTypeContainer) {
appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "Ordered"))
}
case t == reflect.TypeOf(OncePerOrdered):
node.MarkedOncePerOrdered = bool(arg.(honorsOrderedType))
if !nodeType.Is(types.NodeTypeBeforeEach | types.NodeTypeJustBeforeEach | types.NodeTypeAfterEach | types.NodeTypeJustAfterEach) {
appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "OncePerOrdered"))
}
case t == reflect.TypeOf(FlakeAttempts(0)):
node.FlakeAttempts = int(arg.(FlakeAttempts))
if !nodeType.Is(types.NodeTypesForContainerAndIt) {
appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "FlakeAttempts"))
}
case t == reflect.TypeOf(Labels{}):
if !nodeType.Is(types.NodeTypesForContainerAndIt) {
appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "Label"))
}
for _, label := range arg.(Labels) {
if !labelsSeen[label] {
labelsSeen[label] = true
label, err := types.ValidateAndCleanupLabel(label, node.CodeLocation)
node.Labels = append(node.Labels, label)
appendError(err)
}
}
case t.Kind() == reflect.Func:
if node.Body != nil {
appendError(types.GinkgoErrors.MultipleBodyFunctions(node.CodeLocation, nodeType))
trackedFunctionError = true
break
}
isValid := (t.NumOut() == 0) && (t.NumIn() <= 1) && (t.NumIn() == 0 || t.In(0) == reflect.TypeOf(make(Done)))
if !isValid {
appendError(types.GinkgoErrors.InvalidBodyType(t, node.CodeLocation, nodeType))
trackedFunctionError = true
break
}
if t.NumIn() == 0 {
node.Body = arg.(func())
} else {
deprecationTracker.TrackDeprecation(types.Deprecations.Async(), node.CodeLocation)
deprecatedAsyncBody := arg.(func(Done))
node.Body = func() { deprecatedAsyncBody(make(Done)) }
}
default:
remainingArgs = append(remainingArgs, arg)
}
}
//validations
if node.MarkedPending && node.MarkedFocus {
appendError(types.GinkgoErrors.InvalidDeclarationOfFocusedAndPending(node.CodeLocation, nodeType))
}
if node.Body == nil && !node.MarkedPending && !trackedFunctionError {
appendError(types.GinkgoErrors.MissingBodyFunction(node.CodeLocation, nodeType))
}
for _, arg := range remainingArgs {
appendError(types.GinkgoErrors.UnknownDecorator(node.CodeLocation, nodeType, arg))
}
if len(errors) > 0 {
return Node{}, errors
}
return node, errors
}
func NewSynchronizedBeforeSuiteNode(proc1Body func() []byte, allProcsBody func([]byte), codeLocation types.CodeLocation) (Node, []error) {
return Node{
ID: UniqueNodeID(),
NodeType: types.NodeTypeSynchronizedBeforeSuite,
SynchronizedBeforeSuiteProc1Body: proc1Body,
SynchronizedBeforeSuiteAllProcsBody: allProcsBody,
CodeLocation: codeLocation,
}, nil
}
func NewSynchronizedAfterSuiteNode(allProcsBody func(), proc1Body func(), codeLocation types.CodeLocation) (Node, []error) {
return Node{
ID: UniqueNodeID(),
NodeType: types.NodeTypeSynchronizedAfterSuite,
SynchronizedAfterSuiteAllProcsBody: allProcsBody,
SynchronizedAfterSuiteProc1Body: proc1Body,
CodeLocation: codeLocation,
}, nil
}
func NewReportBeforeEachNode(body func(types.SpecReport), codeLocation types.CodeLocation) (Node, []error) {
return Node{
ID: UniqueNodeID(),
NodeType: types.NodeTypeReportBeforeEach,
ReportEachBody: body,
CodeLocation: codeLocation,
NestingLevel: -1,
}, nil
}
func NewReportAfterEachNode(body func(types.SpecReport), codeLocation types.CodeLocation) (Node, []error) {
return Node{
ID: UniqueNodeID(),
NodeType: types.NodeTypeReportAfterEach,
ReportEachBody: body,
CodeLocation: codeLocation,
NestingLevel: -1,
}, nil
}
func NewReportAfterSuiteNode(text string, body func(types.Report), codeLocation types.CodeLocation) (Node, []error) {
return Node{
ID: UniqueNodeID(),
Text: text,
NodeType: types.NodeTypeReportAfterSuite,
ReportAfterSuiteBody: body,
CodeLocation: codeLocation,
}, nil
}
func NewCleanupNode(fail func(string, types.CodeLocation), args ...interface{}) (Node, []error) {
baseOffset := 2
node := Node{
ID: UniqueNodeID(),
NodeType: types.NodeTypeCleanupInvalid,
CodeLocation: types.NewCodeLocation(baseOffset),
NestingLevel: -1,
}
remainingArgs := []interface{}{}
for _, arg := range args {
switch t := reflect.TypeOf(arg); {
case t == reflect.TypeOf(Offset(0)):
node.CodeLocation = types.NewCodeLocation(baseOffset + int(arg.(Offset)))
case t == reflect.TypeOf(types.CodeLocation{}):
node.CodeLocation = arg.(types.CodeLocation)
default:
remainingArgs = append(remainingArgs, arg)
}
}
if len(remainingArgs) == 0 {
return Node{}, []error{types.GinkgoErrors.DeferCleanupInvalidFunction(node.CodeLocation)}
}
callback := reflect.ValueOf(remainingArgs[0])
if !(callback.Kind() == reflect.Func && callback.Type().NumOut() <= 1) {
return Node{}, []error{types.GinkgoErrors.DeferCleanupInvalidFunction(node.CodeLocation)}
}
callArgs := []reflect.Value{}
for _, arg := range remainingArgs[1:] {
callArgs = append(callArgs, reflect.ValueOf(arg))
}
cl := node.CodeLocation
node.Body = func() {
out := callback.Call(callArgs)
if len(out) == 1 && !out[0].IsNil() {
fail(fmt.Sprintf("DeferCleanup callback returned error: %v", out[0]), cl)
}
}
return node, nil
}
func (n Node) IsZero() bool {
return n.ID == 0
}
/* Nodes */
type Nodes []Node
func (n Nodes) CopyAppend(nodes ...Node) Nodes {
numN := len(n)
out := make(Nodes, numN+len(nodes))
for i, node := range n {
out[i] = node
}
for j, node := range nodes {
out[numN+j] = node
}
return out
}
func (n Nodes) SplitAround(pivot Node) (Nodes, Nodes) {
pivotIdx := len(n)
for i := range n {
if n[i].ID == pivot.ID {
pivotIdx = i
break
}
}
left := n[:pivotIdx]
right := Nodes{}
if pivotIdx+1 < len(n) {
right = n[pivotIdx+1:]
}
return left, right
}
func (n Nodes) FirstNodeWithType(nodeTypes types.NodeType) Node {
for i := range n {
if n[i].NodeType.Is(nodeTypes) {
return n[i]
}
}
return Node{}
}
func (n Nodes) WithType(nodeTypes types.NodeType) Nodes {
count := 0
for i := range n {
if n[i].NodeType.Is(nodeTypes) {
count++
}
}
out, j := make(Nodes, count), 0
for i := range n {
if n[i].NodeType.Is(nodeTypes) {
out[j] = n[i]
j++
}
}
return out
}
func (n Nodes) WithoutType(nodeTypes types.NodeType) Nodes {
count := 0
for i := range n {
if !n[i].NodeType.Is(nodeTypes) {
count++
}
}
out, j := make(Nodes, count), 0
for i := range n {
if !n[i].NodeType.Is(nodeTypes) {
out[j] = n[i]
j++
}
}
return out
}
func (n Nodes) WithoutNode(nodeToExclude Node) Nodes {
idxToExclude := len(n)
for i := range n {
if n[i].ID == nodeToExclude.ID {
idxToExclude = i
break
}
}
if idxToExclude == len(n) {
return n
}
out, j := make(Nodes, len(n)-1), 0
for i := range n {
if i == idxToExclude {
continue
}
out[j] = n[i]
j++
}
return out
}
func (n Nodes) Filter(filter func(Node) bool) Nodes {
trufa, count := make([]bool, len(n)), 0
for i := range n {
if filter(n[i]) {
trufa[i] = true
count += 1
}
}
out, j := make(Nodes, count), 0
for i := range n {
if trufa[i] {
out[j] = n[i]
j++
}
}
return out
}
func (n Nodes) FirstSatisfying(filter func(Node) bool) Node {
for i := range n {
if filter(n[i]) {
return n[i]
}
}
return Node{}
}
func (n Nodes) WithinNestingLevel(deepestNestingLevel int) Nodes {
count := 0
for i := range n {
if n[i].NestingLevel <= deepestNestingLevel {
count++
}
}
out, j := make(Nodes, count), 0
for i := range n {
if n[i].NestingLevel <= deepestNestingLevel {
out[j] = n[i]
j++
}
}
return out
}
func (n Nodes) SortedByDescendingNestingLevel() Nodes {
out := make(Nodes, len(n))
copy(out, n)
sort.SliceStable(out, func(i int, j int) bool {
return out[i].NestingLevel > out[j].NestingLevel
})
return out
}
func (n Nodes) SortedByAscendingNestingLevel() Nodes {
out := make(Nodes, len(n))
copy(out, n)
sort.SliceStable(out, func(i int, j int) bool {
return out[i].NestingLevel < out[j].NestingLevel
})
return out
}
func (n Nodes) FirstWithNestingLevel(level int) Node {
for i := range n {
if n[i].NestingLevel == level {
return n[i]
}
}
return Node{}
}
func (n Nodes) Reverse() Nodes {
out := make(Nodes, len(n))
for i := range n {
out[len(n)-1-i] = n[i]
}
return out
}
func (n Nodes) Texts() []string {
out := make([]string, len(n))
for i := range n {
out[i] = n[i].Text
}
return out
}
func (n Nodes) Labels() [][]string {
out := make([][]string, len(n))
for i := range n {
if n[i].Labels == nil {
out[i] = []string{}
} else {
out[i] = []string(n[i].Labels)
}
}
return out
}
func (n Nodes) UnionOfLabels() []string {
out := []string{}
seen := map[string]bool{}
for i := range n {
for _, label := range n[i].Labels {
if !seen[label] {
seen[label] = true
out = append(out, label)
}
}
}
return out
}
func (n Nodes) CodeLocations() []types.CodeLocation {
out := make([]types.CodeLocation, len(n))
for i := range n {
out[i] = n[i].CodeLocation
}
return out
}
func (n Nodes) BestTextFor(node Node) string {
if node.Text != "" {
return node.Text
}
parentNestingLevel := node.NestingLevel - 1
for i := range n {
if n[i].Text != "" && n[i].NestingLevel == parentNestingLevel {
return n[i].Text
}
}
return ""
}
func (n Nodes) ContainsNodeID(id uint) bool {
for i := range n {
if n[i].ID == id {
return true
}
}
return false
}
func (n Nodes) HasNodeMarkedPending() bool {
for i := range n {
if n[i].MarkedPending {
return true
}
}
return false
}
func (n Nodes) HasNodeMarkedFocus() bool {
for i := range n {
if n[i].MarkedFocus {
return true
}
}
return false
}
func (n Nodes) HasNodeMarkedSerial() bool {
for i := range n {
if n[i].MarkedSerial {
return true
}
}
return false
}
func (n Nodes) FirstNodeMarkedOrdered() Node {
for i := range n {
if n[i].MarkedOrdered {
return n[i]
}
}
return Node{}
}
func unrollInterfaceSlice(args interface{}) []interface{} {
v := reflect.ValueOf(args)
if v.Kind() != reflect.Slice {
return []interface{}{args}
}
out := []interface{}{}
for i := 0; i < v.Len(); i++ {
el := reflect.ValueOf(v.Index(i).Interface())
if el.Kind() == reflect.Slice && el.Type() != reflect.TypeOf(Labels{}) {
out = append(out, unrollInterfaceSlice(el.Interface())...)
} else {
out = append(out, v.Index(i).Interface())
}
}
return out
}
Accelerate Your Automation Test Cycles With LambdaTest
Leverage LambdaTestās cloud-based platform to execute your automation tests in parallel and trim down your test execution time significantly. Your first 100 automation testing minutes are on us.