Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable wiring in of DSL customisations #1911

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions karate-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
<exclude>**/*.java</exclude>
</excludes>
</testResource>
<testResource>
<directory>src/test/resources</directory>
</testResource>
</testResources>
<plugins>
<plugin>
Expand Down
5 changes: 5 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.intuit.karate;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -152,4 +153,8 @@ public interface Actions {

void robot(String exp);

default List<ExtensibleActions> additionalActions() {
return Collections.emptyList();
}

}
21 changes: 21 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/ExtensibleActions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.intuit.karate;

import com.intuit.karate.core.ScenarioEngine;

/**
* Defines the mechanisms to wire in user defined customizations.
*/
public interface ExtensibleActions {

/**
* @return - The current implementation class name.
*/
Class<? extends ExtensibleActions> implementationClass();

/**
* @param engine - The {@link ScenarioEngine} object from Karate that may be required for the user
* defined customization.
*/
void initialiseEngine(ScenarioEngine engine);

}
Original file line number Diff line number Diff line change
Expand Up @@ -420,4 +420,8 @@ public void robot(String exp) {
engine.robot(exp);
}

@Override
public List<ExtensibleActions> additionalActions() {
return engine.getExtensibleActions();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.intuit.karate.core;

import com.intuit.karate.ExtensibleActions;
import com.intuit.karate.FileUtils;
import com.intuit.karate.Json;
import com.intuit.karate.JsonUtils;
Expand All @@ -47,6 +48,8 @@
import com.intuit.karate.template.KarateTemplateEngine;
import com.intuit.karate.template.TemplateUtils;
import com.jayway.jsonpath.PathNotFoundException;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.graalvm.polyglot.Value;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
Expand Down Expand Up @@ -131,6 +134,12 @@ public ScenarioEngine(Config config, ScenarioRuntime runtime, Map<String, Variab
this.vars = vars;
this.logger = logger;
this.requestBuilder = requestBuilder;
Iterator<ExtensibleActions> it = ServiceLoader.load(ExtensibleActions.class).iterator();
Iterable<ExtensibleActions> iterable = () -> it;
extensibleActions = StreamSupport.stream(iterable.spliterator(),
false)
.peek(each -> each.initialiseEngine(this))
.collect(Collectors.toList());
}

public static ScenarioEngine forTempUse(HttpClientFactory hcf) {
Expand Down Expand Up @@ -292,11 +301,16 @@ public void capturePerfEvent(PerfEvent event) {
private HttpRequest request;
private Response response;
private Config config;
private final List<ExtensibleActions> extensibleActions;

public Config getConfig() {
return config;
}

public List<ExtensibleActions> getExtensibleActions() {
return extensibleActions;
}

// important: use this to trigger client re-config
// callonce routine is one example
public void setConfig(Config config) {
Expand Down
78 changes: 52 additions & 26 deletions karate-core/src/main/java/com/intuit/karate/core/StepRuntime.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand All @@ -57,12 +60,14 @@ private StepRuntime() {

static class MethodPattern {

final Class<?> clazz;
final String regex;
final Method method;
final Pattern pattern;
final String keyword;

MethodPattern(Method method, String regex) {
MethodPattern(Class<?> clazz, Method method, String regex) {
this.clazz = clazz;
this.regex = regex;
this.method = method;
try {
Expand All @@ -78,7 +83,7 @@ static class MethodPattern {
List<String> match(String text) {
Matcher matcher = pattern.matcher(text);
if (matcher.lookingAt()) {
List<String> args = new ArrayList(matcher.groupCount());
List<String> 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));
Expand All @@ -100,20 +105,22 @@ 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 Class<?> clazz;
final Method method;
final List<String> args;

MethodMatch(Method method, List<String> args) {
this.clazz = method.getDeclaringClass();
this.method = method;
this.args = args;
}

Object[] convertArgs(Object last) {
Class[] types = method.getParameterTypes();
Class<?>[] types = method.getParameterTypes();
Object[] result = new Object[types.length];
int i = 0;
for (String arg : args) {
Class type = types[i];
Class<?> type = types[i];
if (List.class.isAssignableFrom(type)) {
result[i] = StringUtils.split(arg, ',', false);
} else if (int.class.isAssignableFrom(type)) {
Expand Down Expand Up @@ -180,33 +187,45 @@ public String toString() {
sb.append(sj);
sb.append(")");

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

}

private static final Collection<MethodPattern> PATTERNS;
private static final Map<String, Collection<Method>> KEYWORDS_METHODS;
public static final Collection<Method> METHOD_MATCH;
private static final Collection<MethodPattern> PATTERNS = new HashSet<>();
private static final Map<String, Collection<Method>> KEYWORDS_METHODS = new ConcurrentHashMap<>();
public static final Collection<Method> METHOD_MATCH = new HashSet<>();
private static final Set<Class<?>> processedClasses = new HashSet<>();

static {
Map<String, MethodPattern> temp = new HashMap();
List<MethodPattern> overwrite = new ArrayList();
KEYWORDS_METHODS = new HashMap();
for (Method method : ScenarioActions.class.getMethods()) {
initialise(ScenarioActions.class);
}

public static void initialise(Class<?> clazz) {
if (processedClasses.contains(clazz)) {
return;
}
processedClasses.add(clazz);
Map<String, MethodPattern> temp = new HashMap<>();
List<MethodPattern> overwrite = new ArrayList<>();
List<Method> methods = Arrays.stream(clazz.getMethods())
.filter(each -> each.getDeclaredAnnotation(When.class) != null ||
each.getDeclaredAnnotation(Action.class) != null).collect(Collectors.toList());
for (Method method : methods) {
When when = method.getDeclaredAnnotation(When.class);
if (when != null) {
String regex = when.value();
MethodPattern methodPattern = new MethodPattern(method, regex);
MethodPattern methodPattern = new MethodPattern(clazz, method, regex);
temp.put(regex, methodPattern);

Collection<Method> keywordMethods = KEYWORDS_METHODS.computeIfAbsent(methodPattern.keyword, k -> new HashSet<>());
keywordMethods.add(methodPattern.method);
StepRuntime.KEYWORDS_METHODS
.computeIfAbsent(methodPattern.keyword, k -> new HashSet<>())
.add(methodPattern.method);
} else {
Action action = method.getDeclaredAnnotation(Action.class);
if (action != null) {
String regex = action.value();
MethodPattern methodPattern = new MethodPattern(method, regex);
MethodPattern methodPattern = new MethodPattern(clazz, method, regex);
overwrite.add(methodPattern);
}
}
Expand All @@ -215,15 +234,16 @@ public String toString() {
for (MethodPattern mp : overwrite) {
temp.put(mp.regex, mp);

Collection<Method> keywordMethods = KEYWORDS_METHODS.computeIfAbsent(mp.keyword, k -> new HashSet<>());
keywordMethods.add(mp.method);
StepRuntime.KEYWORDS_METHODS
.computeIfAbsent(mp.keyword, k -> new HashSet<>())
.add(mp.method);
}
PATTERNS = temp.values();
METHOD_MATCH = findMethodsByKeyword("match");
PATTERNS.addAll(temp.values());
METHOD_MATCH.addAll(findMethodsByKeyword("match"));
}

private static List<MethodMatch> findMethodsMatching(String text) {
List<MethodMatch> matches = new ArrayList(1);
List<MethodMatch> matches = new ArrayList<>(1);
for (MethodPattern pattern : PATTERNS) {
List<String> args = pattern.match(text);
if (args != null) {
Expand All @@ -234,10 +254,8 @@ private static List<MethodMatch> findMethodsMatching(String text) {
}

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

Expand All @@ -250,6 +268,7 @@ private static long getElapsedTimeNanos(long startTime) {
}

public static Result execute(Step step, Actions actions) {
actions.additionalActions().forEach(each -> initialise(each.implementationClass()));
String text = step.getText();
List<MethodMatch> matches = findMethodsMatching(text);
if (matches.isEmpty()) {
Expand Down Expand Up @@ -277,7 +296,14 @@ public static Result execute(Step step, Actions actions) {
}
long startTime = System.nanoTime();
try {
match.method.invoke(actions, args);
Class<?> belongsTo = match.method.getDeclaringClass();
AtomicReference<Object> valueToPass = new AtomicReference<>(actions);
if (!belongsTo.equals(ScenarioActions.class)) {
actions.additionalActions().stream()
.filter(each -> each.implementationClass().equals(belongsTo))
.findFirst().ifPresent(valueToPass::set);
}
match.method.invoke(valueToPass.get(), args);
if (actions.isAborted()) {
return Result.aborted(getElapsedTimeNanos(startTime), match);
} else if (actions.isFailed()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.intuit.karate.core.extensions;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import org.junit.jupiter.api.Test;

public class GreetTest {

@Test
public void testMethod() {
Results results = Runner.path("classpath:com/intuit/karate/core/extensions/greet.feature")
.parallel(1);
assertEquals(0, results.getFailCount(), results.getErrorMessages());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.intuit.karate.core.extensions;

import com.intuit.karate.ExtensibleActions;
import com.intuit.karate.core.ScenarioEngine;
import cucumber.api.java.en.When;

public class Greetings implements ExtensibleActions {

private ScenarioEngine engine;

@Override
public Class<? extends ExtensibleActions> implementationClass() {
return getClass();
}

@Override
public void initialiseEngine(ScenarioEngine engine) {
this.engine = engine;

}

@When("^greet (.+)")
public void greet(String exp) {
engine.print(exp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Feature: Greet Feature

Scenario: Greet scenario
* def name = 'Dragon Warrior'
* greet name
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.intuit.karate.core.extensions.Greetings