io.cucumber.core.runner.CachingGlue Maven / Gradle / Ivy
The newest version!
package io.cucumber.core.runner;
import io.cucumber.core.backend.DataTableTypeDefinition;
import io.cucumber.core.backend.DefaultDataTableCellTransformerDefinition;
import io.cucumber.core.backend.DefaultDataTableEntryTransformerDefinition;
import io.cucumber.core.backend.DefaultParameterTransformerDefinition;
import io.cucumber.core.backend.DocStringTypeDefinition;
import io.cucumber.core.backend.Glue;
import io.cucumber.core.backend.HookDefinition;
import io.cucumber.core.backend.JavaMethodReference;
import io.cucumber.core.backend.ParameterTypeDefinition;
import io.cucumber.core.backend.ScenarioScoped;
import io.cucumber.core.backend.StackTraceElementReference;
import io.cucumber.core.backend.StaticHookDefinition;
import io.cucumber.core.backend.StepDefinition;
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.gherkin.Step;
import io.cucumber.core.stepexpression.Argument;
import io.cucumber.core.stepexpression.StepExpression;
import io.cucumber.core.stepexpression.StepExpressionFactory;
import io.cucumber.core.stepexpression.StepTypeRegistry;
import io.cucumber.cucumberexpressions.CucumberExpression;
import io.cucumber.cucumberexpressions.Expression;
import io.cucumber.cucumberexpressions.ParameterByTypeTransformer;
import io.cucumber.cucumberexpressions.ParameterType;
import io.cucumber.cucumberexpressions.RegularExpression;
import io.cucumber.datatable.TableCellByTypeTransformer;
import io.cucumber.datatable.TableEntryByTypeTransformer;
import io.cucumber.messages.types.Envelope;
import io.cucumber.messages.types.Hook;
import io.cucumber.messages.types.JavaMethod;
import io.cucumber.messages.types.JavaStackTraceElement;
import io.cucumber.messages.types.Location;
import io.cucumber.messages.types.SourceReference;
import io.cucumber.messages.types.StepDefinitionPattern;
import io.cucumber.messages.types.StepDefinitionPatternType;
import io.cucumber.plugin.event.StepDefinedEvent;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
final class CachingGlue implements Glue {
private static final Comparator HOOK_ORDER_ASCENDING = Comparator
.comparingInt(CoreHookDefinition::getOrder)
.thenComparing(ScenarioScoped.class::isInstance);
private static final Comparator STATIC_HOOK_ORDER_ASCENDING = Comparator
.comparingInt(StaticHookDefinition::getOrder);
private final List parameterTypeDefinitions = new ArrayList<>();
private final List dataTableTypeDefinitions = new ArrayList<>();
private final List defaultParameterTransformers = new ArrayList<>();
private final List defaultDataTableEntryTransformers = new ArrayList<>();
private final List defaultDataTableCellTransformers = new ArrayList<>();
private final List docStringTypeDefinitions = new ArrayList<>();
private final List beforeAllHooks = new ArrayList<>();
private final List beforeHooks = new ArrayList<>();
private final List beforeStepHooks = new ArrayList<>();
private final List stepDefinitions = new ArrayList<>();
private final List afterStepHooks = new ArrayList<>();
private final List afterHooks = new ArrayList<>();
private final List afterAllHooks = new ArrayList<>();
/*
* Storing the pattern that matches the step text allows us to cache the
* rather slow regex comparisons in `stepDefinitionMatches`. This cache does
* not need to be cleaned. The matching pattern be will used to look up a
* pickle specific step definition from `stepDefinitionsByPattern`.
*/
private final Map stepPatternByStepText = new HashMap<>();
private final Map stepDefinitionsByPattern = new TreeMap<>();
private final EventBus bus;
CachingGlue(EventBus bus) {
this.bus = bus;
}
@Override
public void addBeforeAllHook(StaticHookDefinition beforeAllHook) {
beforeAllHooks.add(beforeAllHook);
beforeAllHooks.sort(STATIC_HOOK_ORDER_ASCENDING);
}
@Override
public void addAfterAllHook(StaticHookDefinition afterAllHook) {
afterAllHooks.add(afterAllHook);
afterAllHooks.sort(STATIC_HOOK_ORDER_ASCENDING);
}
@Override
public void addStepDefinition(StepDefinition stepDefinition) {
stepDefinitions.add(stepDefinition);
}
@Override
public void addBeforeHook(HookDefinition hookDefinition) {
beforeHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
beforeHooks.sort(HOOK_ORDER_ASCENDING);
}
@Override
public void addAfterHook(HookDefinition hookDefinition) {
afterHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
afterHooks.sort(HOOK_ORDER_ASCENDING);
}
@Override
public void addBeforeStepHook(HookDefinition hookDefinition) {
beforeStepHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
beforeStepHooks.sort(HOOK_ORDER_ASCENDING);
}
@Override
public void addAfterStepHook(HookDefinition hookDefinition) {
afterStepHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
afterStepHooks.sort(HOOK_ORDER_ASCENDING);
}
@Override
public void addParameterType(ParameterTypeDefinition parameterType) {
parameterTypeDefinitions.add(parameterType);
}
@Override
public void addDataTableType(DataTableTypeDefinition dataTableType) {
dataTableTypeDefinitions.add(dataTableType);
}
@Override
public void addDefaultParameterTransformer(DefaultParameterTransformerDefinition defaultParameterTransformer) {
defaultParameterTransformers.add(defaultParameterTransformer);
}
@Override
public void addDefaultDataTableEntryTransformer(
DefaultDataTableEntryTransformerDefinition defaultDataTableEntryTransformer
) {
defaultDataTableEntryTransformers
.add(CoreDefaultDataTableEntryTransformerDefinition.create(defaultDataTableEntryTransformer));
}
@Override
public void addDefaultDataTableCellTransformer(
DefaultDataTableCellTransformerDefinition defaultDataTableCellTransformer
) {
defaultDataTableCellTransformers.add(defaultDataTableCellTransformer);
}
@Override
public void addDocStringType(DocStringTypeDefinition docStringType) {
docStringTypeDefinitions.add(docStringType);
}
List getBeforeAllHooks() {
return new ArrayList<>(beforeAllHooks);
}
Collection getBeforeHooks() {
return new ArrayList<>(beforeHooks);
}
Collection getBeforeStepHooks() {
return new ArrayList<>(beforeStepHooks);
}
Collection getAfterHooks() {
List hooks = new ArrayList<>(afterHooks);
Collections.reverse(hooks);
return hooks;
}
Collection getAfterStepHooks() {
List hooks = new ArrayList<>(afterStepHooks);
Collections.reverse(hooks);
return hooks;
}
List getAfterAllHooks() {
ArrayList hooks = new ArrayList<>(afterAllHooks);
Collections.reverse(hooks);
return hooks;
}
Collection getParameterTypeDefinitions() {
return parameterTypeDefinitions;
}
Collection getDataTableTypeDefinitions() {
return dataTableTypeDefinitions;
}
Collection getStepDefinitions() {
return stepDefinitions;
}
Map getStepPatternByStepText() {
return stepPatternByStepText;
}
Map getStepDefinitionsByPattern() {
return stepDefinitionsByPattern;
}
Collection getDefaultParameterTransformers() {
return defaultParameterTransformers;
}
Collection getDefaultDataTableEntryTransformers() {
return defaultDataTableEntryTransformers;
}
Collection getDefaultDataTableCellTransformers() {
return defaultDataTableCellTransformers;
}
List getDocStringTypeDefinitions() {
return docStringTypeDefinitions;
}
void prepareGlue(StepTypeRegistry stepTypeRegistry) throws DuplicateStepDefinitionException {
StepExpressionFactory stepExpressionFactory = new StepExpressionFactory(stepTypeRegistry, bus);
// TODO: separate prepared and unprepared glue into different classes
parameterTypeDefinitions.forEach(ptd -> {
ParameterType> parameterType = ptd.parameterType();
stepTypeRegistry.defineParameterType(parameterType);
emitParameterTypeDefined(ptd);
});
dataTableTypeDefinitions.forEach(dtd -> stepTypeRegistry.defineDataTableType(dtd.dataTableType()));
docStringTypeDefinitions.forEach(dtd -> stepTypeRegistry.defineDocStringType(dtd.docStringType()));
if (defaultParameterTransformers.size() == 1) {
DefaultParameterTransformerDefinition definition = defaultParameterTransformers.get(0);
ParameterByTypeTransformer transformer = definition.parameterByTypeTransformer();
stepTypeRegistry.setDefaultParameterTransformer(transformer);
} else if (defaultParameterTransformers.size() > 1) {
throw new DuplicateDefaultParameterTransformers(defaultParameterTransformers);
}
if (defaultDataTableEntryTransformers.size() == 1) {
DefaultDataTableEntryTransformerDefinition definition = defaultDataTableEntryTransformers.get(0);
TableEntryByTypeTransformer transformer = definition.tableEntryByTypeTransformer();
stepTypeRegistry.setDefaultDataTableEntryTransformer(transformer);
} else if (defaultDataTableEntryTransformers.size() > 1) {
throw new DuplicateDefaultDataTableEntryTransformers(defaultDataTableEntryTransformers);
}
if (defaultDataTableCellTransformers.size() == 1) {
DefaultDataTableCellTransformerDefinition definition = defaultDataTableCellTransformers.get(0);
TableCellByTypeTransformer transformer = definition.tableCellByTypeTransformer();
stepTypeRegistry.setDefaultDataTableCellTransformer(transformer);
} else if (defaultDataTableCellTransformers.size() > 1) {
throw new DuplicateDefaultDataTableCellTransformers(defaultDataTableCellTransformers);
}
// TODO: Redefine hooks for each scenario, similar to how we're doing
// for CoreStepDefinition
beforeHooks.forEach(this::emitHook);
beforeStepHooks.forEach(this::emitHook);
stepDefinitions.forEach(stepDefinition -> {
StepExpression expression = stepExpressionFactory.createExpression(stepDefinition);
CoreStepDefinition coreStepDefinition = new CoreStepDefinition(bus.generateId(), stepDefinition,
expression);
CoreStepDefinition previous = stepDefinitionsByPattern.get(stepDefinition.getPattern());
if (previous != null) {
throw new DuplicateStepDefinitionException(previous, stepDefinition);
}
stepDefinitionsByPattern.put(coreStepDefinition.getExpression().getSource(), coreStepDefinition);
emitStepDefined(coreStepDefinition);
});
afterStepHooks.forEach(this::emitHook);
afterHooks.forEach(this::emitHook);
}
private void emitParameterTypeDefined(ParameterTypeDefinition parameterTypeDefinition) {
ParameterType> parameterType = parameterTypeDefinition.parameterType();
io.cucumber.messages.types.ParameterType messagesParameterType = new io.cucumber.messages.types.ParameterType(
parameterType.getName(),
parameterType.getRegexps(),
parameterType.preferForRegexpMatch(),
parameterType.useForSnippets(),
bus.generateId().toString(),
parameterTypeDefinition.getSourceReference()
.map(this::createSourceReference)
.orElseGet(this::emptySourceReference));
bus.send(Envelope.of(messagesParameterType));
}
private void emitHook(CoreHookDefinition coreHook) {
Hook messagesHook = new Hook(
coreHook.getId().toString(),
null,
coreHook.getDefinitionLocation()
.map(this::createSourceReference)
.orElseGet(this::emptySourceReference),
coreHook.getTagExpression());
bus.send(Envelope.of(messagesHook));
}
private void emitStepDefined(CoreStepDefinition coreStepDefinition) {
bus.send(new StepDefinedEvent(
bus.getInstant(),
new io.cucumber.plugin.event.StepDefinition(
coreStepDefinition.getStepDefinition().getLocation(),
coreStepDefinition.getExpression().getSource())));
io.cucumber.messages.types.StepDefinition messagesStepDefinition = new io.cucumber.messages.types.StepDefinition(
coreStepDefinition.getId().toString(),
new StepDefinitionPattern(
coreStepDefinition.getExpression().getSource(),
getExpressionType(coreStepDefinition)),
coreStepDefinition.getDefinitionLocation()
.map(this::createSourceReference)
.orElseGet(this::emptySourceReference));
bus.send(Envelope.of(messagesStepDefinition));
}
private SourceReference createSourceReference(io.cucumber.core.backend.SourceReference reference) {
if (reference instanceof JavaMethodReference) {
JavaMethodReference methodReference = (JavaMethodReference) reference;
return SourceReference.of(new JavaMethod(
methodReference.className(),
methodReference.methodName(),
methodReference.methodParameterTypes()));
}
if (reference instanceof StackTraceElementReference) {
StackTraceElementReference stackReference = (StackTraceElementReference) reference;
JavaStackTraceElement stackTraceElement = new JavaStackTraceElement(
stackReference.className(),
// TODO: Fix json schema. Stacktrace elements need not have a
// source file
stackReference.fileName().orElse("Unknown"),
stackReference.methodName());
Location location = new Location((long) stackReference.lineNumber(), null);
return new SourceReference(null, null, stackTraceElement, location);
}
return emptySourceReference();
}
private SourceReference emptySourceReference() {
return new SourceReference(null, null, null, null);
}
private StepDefinitionPatternType getExpressionType(CoreStepDefinition stepDefinition) {
Class extends Expression> expressionType = stepDefinition.getExpression().getExpressionType();
if (expressionType.isAssignableFrom(RegularExpression.class)) {
return StepDefinitionPatternType.REGULAR_EXPRESSION;
} else if (expressionType.isAssignableFrom(CucumberExpression.class)) {
return StepDefinitionPatternType.CUCUMBER_EXPRESSION;
} else {
throw new IllegalArgumentException(expressionType.getName());
}
}
PickleStepDefinitionMatch stepDefinitionMatch(URI uri, Step step) throws AmbiguousStepDefinitionsException {
PickleStepDefinitionMatch cachedMatch = cachedStepDefinitionMatch(uri, step);
if (cachedMatch != null) {
return cachedMatch;
}
return findStepDefinitionMatch(uri, step);
}
private PickleStepDefinitionMatch cachedStepDefinitionMatch(URI uri, Step step) {
String stepDefinitionPattern = stepPatternByStepText.get(step.getText());
if (stepDefinitionPattern == null) {
return null;
}
CoreStepDefinition coreStepDefinition = stepDefinitionsByPattern.get(stepDefinitionPattern);
if (coreStepDefinition == null) {
return null;
}
// Step definition arguments consists of parameters included in the step
// text and
// gherkin step arguments (doc string and data table) which are not
// included in
// the step text. As such the step definition arguments can not be
// cached and
// must be recreated each time.
List arguments = coreStepDefinition.matchedArguments(step);
return new PickleStepDefinitionMatch(arguments, coreStepDefinition, uri, step);
}
private PickleStepDefinitionMatch findStepDefinitionMatch(URI uri, Step step)
throws AmbiguousStepDefinitionsException {
List matches = stepDefinitionMatches(uri, step);
if (matches.isEmpty()) {
return null;
}
if (matches.size() > 1) {
throw new AmbiguousStepDefinitionsException(step, matches);
}
PickleStepDefinitionMatch match = matches.get(0);
stepPatternByStepText.put(step.getText(), match.getPattern());
return match;
}
private List stepDefinitionMatches(URI uri, Step step) {
List result = new ArrayList<>();
for (CoreStepDefinition coreStepDefinition : stepDefinitionsByPattern.values()) {
List arguments = coreStepDefinition.matchedArguments(step);
if (arguments != null) {
result.add(new PickleStepDefinitionMatch(arguments, coreStepDefinition, uri, step));
}
}
return result;
}
void removeScenarioScopedGlue() {
stepDefinitionsByPattern.clear();
removeScenarioScopedGlue(beforeHooks);
removeScenarioScopedGlue(beforeStepHooks);
removeScenarioScopedGlue(afterHooks);
removeScenarioScopedGlue(afterStepHooks);
removeScenarioScopedGlue(stepDefinitions);
removeScenarioScopedGlue(dataTableTypeDefinitions);
removeScenarioScopedGlue(docStringTypeDefinitions);
removeScenarioScopedGlue(parameterTypeDefinitions);
removeScenarioScopedGlue(defaultParameterTransformers);
removeScenarioScopedGlue(defaultDataTableEntryTransformers);
removeScenarioScopedGlue(defaultDataTableCellTransformers);
}
private void removeScenarioScopedGlue(Iterable> glues) {
Iterator> glueIterator = glues.iterator();
while (glueIterator.hasNext()) {
Object glue = glueIterator.next();
if (glue instanceof ScenarioScoped) {
ScenarioScoped scenarioScoped = (ScenarioScoped) glue;
scenarioScoped.dispose();
glueIterator.remove();
}
}
}
}