All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intuit.karate.core.StepRuntime Maven / Gradle / Ivy

The newest version!
/*
 * The MIT License
 *
 * Copyright 2022 Karate Labs Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.intuit.karate.core;

import com.intuit.karate.Actions;
import com.intuit.karate.Json;
import com.intuit.karate.JsonUtils;
import com.intuit.karate.KarateException;
import com.intuit.karate.ScenarioActions;
import com.intuit.karate.StringUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.LoggerFactory;

/**
 * @author pthomas3
 */
public class StepRuntime {

    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(StepRuntime.class);

    private StepRuntime() {
        // only static methods
    }

    static class MethodPattern {

        final String regex;
        final Method method;
        final Pattern pattern;
        final String keyword;

        MethodPattern(Method method, String regex) {
            String keyword1;
            this.regex = regex;
            this.method = method;
            try {
                pattern = Pattern.compile(regex);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            // assuming all @When or @Action start with a ^, get the first word
            keyword1 = regex.substring(1).split(" |\\\\h|\\\\s")[0];
            if (keyword1.endsWith("$")) { // if ends with $ most likely it's a doc string (i.e. the """ for multiline strings)
                keyword1 = keyword1.substring(0, keyword1.length() - 1);
            }
            keyword = keyword1;
        }

        List match(String text) {
            Matcher matcher = pattern.matcher(text);
            if (matcher.lookingAt()) {
                List args = new ArrayList(matcher.groupCount());
                for (int i = 1; i <= matcher.groupCount(); i++) {
                    int startIndex = matcher.start(i);
                    args.add(startIndex == -1 ? null : matcher.group(i));
                }
                return args;
            } else {
                return null;
            }
        }

        @Override
        public String toString() {
            return "\n" + pattern + " " + method.toGenericString();
        }

    }

    public static class MethodMatch {

        private static final Pattern METHOD_REGEX_PATTERN = Pattern.compile("([a-zA-Z_$][a-zA-Z\\d_$\\.]*)*\\.([a-zA-Z_$][a-zA-Z\\d_$]*?)\\((.*)\\)");

        final Method method;
        final List args;

        MethodMatch(Method method, List args) {
            this.method = method;
            this.args = args;
        }

        Object[] convertArgs(Object last) {
            Class[] types = method.getParameterTypes();
            Object[] result = new Object[types.length];
            int i = 0;
            for (String arg : args) {
                Class type = types[i];
                if (List.class.isAssignableFrom(type)) {
                    result[i] = StringUtils.split(arg, ',', false);
                } else if (int.class.isAssignableFrom(type)) {
                    result[i] = Integer.valueOf(arg);
                } else { // string
                    result[i] = arg;
                }
                i++;
            }
            if (last != null) {
                result[i] = last;
            }
            return result;
        }

        public static MethodMatch getBySignatureAndArgs(String methodReference) {

            String methodSignature = methodReference.substring(0, methodReference.indexOf(' '));
            String referenceArgs = methodReference.substring(methodReference.indexOf(' ') + 1);
            Matcher methodMatch = METHOD_REGEX_PATTERN.matcher(methodSignature);

            Method method = null;
            if (methodMatch.find()) {
                try {
                    String className = methodMatch.group(1);
                    String methodName = methodMatch.group(2);
                    String params = methodMatch.group(3);
                    List paramList = Arrays.asList(params.split(","));
                    method = Class.forName(className).getMethod(methodName, paramList.stream().map(param -> {
                        try {
                            return Class.forName(param);
                        } catch (ClassNotFoundException e) {
                            return null;
                        }
                    }).filter(Objects::nonNull).toArray(Class[]::new));
                } catch (ClassNotFoundException | NoSuchMethodException e) {
                    return null;
                }
            }

            List args = "null".equalsIgnoreCase(referenceArgs) ? null : Json.of(JsonUtils.fromJson(referenceArgs)).asList();
            return new MethodMatch(method, args);
        }

        public Method getMethod() {
            return method;
        }

        public List getArgs() {
            return args;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(method.getDeclaringClass().getName());
            sb.append(".");
            sb.append(method.getName());
            sb.append("(");
            StringJoiner sj = new StringJoiner(",");
            for (Class parameterType : method.getParameterTypes()) {
                sj.add(parameterType.getTypeName());
            }
            sb.append(sj);
            sb.append(")");

            return sb.toString() + " " + (args == null || args.isEmpty() ? "null" : JsonUtils.toJson(args));
        }

    }

    private static final Collection PATTERNS;
    private static final Map> KEYWORDS_METHODS;
    public static final Collection METHOD_MATCH;

    static {
        Map temp = new HashMap();
        List overwrite = new ArrayList();
        KEYWORDS_METHODS = new HashMap();
        for (Method method : ScenarioActions.class.getMethods()) {
            When when = method.getDeclaredAnnotation(When.class);
            if (when != null) {
                String regex = when.value();
                MethodPattern methodPattern = new MethodPattern(method, regex);
                temp.put(regex, methodPattern);
                // edge case for eval() method it's mean to match anything in a line e.g. waitFor('#stuff')
                // regex is ([\w]+)([^\s^\w])(.+)
                String keyword = method.getName().equalsIgnoreCase("eval") ? "eval" : methodPattern.keyword;
                Collection keywordMethods = KEYWORDS_METHODS.computeIfAbsent(keyword, k -> new HashSet<>());
                keywordMethods.add(methodPattern.method);
            }
        }
        for (MethodPattern mp : overwrite) {
            temp.put(mp.regex, mp);

            String keyword = mp.method.getName().equalsIgnoreCase("eval") ? "eval" : mp.keyword;
            Collection keywordMethods = KEYWORDS_METHODS.computeIfAbsent(keyword, k -> new HashSet<>());
            keywordMethods.add(mp.method);
        }
        PATTERNS = temp.values();
        METHOD_MATCH = findMethodsByKeyword("match");
    }

    private static List findMethodsMatching(String text) {
        List matches = new ArrayList(1);
        for (MethodPattern pattern : PATTERNS) {
            List args = pattern.match(text);
            if (args != null) {
                matches.add(new MethodMatch(pattern.method, args));
            }
        }
        return matches;
    }

    public static Collection findMethodsByKeywords(List text) {
        Collection methods = new HashSet();
        text.forEach(m -> {
            methods.addAll(findMethodsByKeyword(m));
        });
        return methods;
    }

    public static Collection findMethodsByKeyword(String text) {
        if (KEYWORDS_METHODS.get(text) != null) {
            return KEYWORDS_METHODS.get(text);
        } else {
            LOGGER.warn("No keyword found for {}. Potential unexpected behavior.", text);
            return new HashSet<>();
        }
    }

    private static long getElapsedTimeNanos(long startTime) {
        return System.nanoTime() - startTime;
    }

    public static Result execute(Step step, Actions actions) {
        String text = step.getText();
        List matches = findMethodsMatching(text);
        if (matches.isEmpty()) {
            KarateException e = new KarateException("no step-definition method match found for: " + text);
            return Result.failed(System.currentTimeMillis(), 0, e, step);
        } else if (matches.size() > 1) {
            KarateException e = new KarateException("more than one step-definition method matched: " + text + " - " + matches);
            return Result.failed(System.currentTimeMillis(), 0, e, step);
        }
        MethodMatch match = matches.get(0);
        Object last;
        if (step.getDocString() != null) {
            last = step.getDocString();
        } else if (step.getTable() != null) {
            last = step.getTable().getRowsAsMaps();
        } else {
            last = null;
        }
        Object[] args;
        try {
            args = match.convertArgs(last);
        } catch (Exception ignored) { // edge case where user error causes [request =] to match [request docstring]
            KarateException e = new KarateException("no step-definition method match found for: " + text);
            return Result.failed(System.currentTimeMillis(), 0, e, step);
        }
        final long startTime = System.currentTimeMillis();
        final long startTimeNanos = System.nanoTime();
        try {
            match.method.invoke(actions, args);
            final long elapsedTimeNanos = getElapsedTimeNanos(startTimeNanos);
            if (actions.isAborted()) {
                return Result.aborted(startTime, elapsedTimeNanos, match);
            } else if (actions.isFailed()) {
                return Result.failed(startTime, elapsedTimeNanos, actions.getFailedReason(), step, match);
            } else {
                return Result.passed(startTime, elapsedTimeNanos, match);
            }
        } catch (InvocationTargetException e) {
            return Result.failed(startTime, getElapsedTimeNanos(startTimeNanos), e.getTargetException(), step, match);
        } catch (Exception e) {
            return Result.failed(startTime, getElapsedTimeNanos(startTimeNanos), e, step, match);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy