diff --git a/jbehave-core/src/main/java/org/jbehave/core/configuration/Configuration.java b/jbehave-core/src/main/java/org/jbehave/core/configuration/Configuration.java index dad102b4a..db40d044a 100755 --- a/jbehave-core/src/main/java/org/jbehave/core/configuration/Configuration.java +++ b/jbehave-core/src/main/java/org/jbehave/core/configuration/Configuration.java @@ -13,6 +13,8 @@ import org.jbehave.core.condition.StepConditionMatcher; import org.jbehave.core.embedder.Embedder; import org.jbehave.core.embedder.StoryControls; +import org.jbehave.core.expressions.ExpressionResolver; +import org.jbehave.core.expressions.NullExpressionResolverMonitor; import org.jbehave.core.failures.FailingUponPendingStep; import org.jbehave.core.failures.FailureStrategy; import org.jbehave.core.failures.PassingUponPendingStep; @@ -187,6 +189,11 @@ public abstract class Configuration { */ protected ParameterConverters parameterConverters; + /** + * Use default built-in expression resolver + */ + protected ExpressionResolver expressionResolver; + /** * Use default built-in ExamplesTable parsers */ @@ -409,6 +416,13 @@ public ParameterConverters parameterConverters() { return parameterConverters; } + public ExpressionResolver expressionResolver() { + if (expressionResolver == null) { + expressionResolver = new ExpressionResolver(Collections.emptySet(), new NullExpressionResolverMonitor()); + } + return expressionResolver; + } + public TableParsers tableParsers() { if (tableParsers == null) { tableParsers = new TableParsers(keywords(), parameterConverters(), Optional.empty()); @@ -580,6 +594,11 @@ public Configuration useParameterConverters(ParameterConverters parameterConvert return this; } + public Configuration useExpressionResolver(ExpressionResolver expressionResolver) { + this.expressionResolver = expressionResolver; + return this; + } + public Configuration useTableTransformers(TableTransformers tableTransformers) { this.tableTransformers = tableTransformers; return this; diff --git a/jbehave-core/src/main/java/org/jbehave/core/configuration/UnmodifiableConfiguration.java b/jbehave-core/src/main/java/org/jbehave/core/configuration/UnmodifiableConfiguration.java index ffeeddfe6..e0b395129 100755 --- a/jbehave-core/src/main/java/org/jbehave/core/configuration/UnmodifiableConfiguration.java +++ b/jbehave-core/src/main/java/org/jbehave/core/configuration/UnmodifiableConfiguration.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.builder.ToStringStyle; import org.jbehave.core.condition.StepConditionMatcher; import org.jbehave.core.embedder.StoryControls; +import org.jbehave.core.expressions.ExpressionResolver; import org.jbehave.core.failures.FailureStrategy; import org.jbehave.core.failures.PendingStepStrategy; import org.jbehave.core.io.StoryLoader; @@ -89,6 +90,11 @@ public ParameterConverters parameterConverters() { return delegate.parameterConverters(); } + @Override + public ExpressionResolver expressionResolver() { + return delegate.expressionResolver(); + } + @Override public ParameterControls parameterControls() { return delegate.parameterControls(); @@ -228,7 +234,12 @@ public Configuration useDefaultStoryReporter(StoryReporter storyReporter) { public Configuration useParameterConverters(ParameterConverters parameterConverters) { throw notAllowed(); } - + + @Override + public Configuration useExpressionResolver(ExpressionResolver expressionResolver) { + throw notAllowed(); + } + @Override public Configuration useParameterControls(ParameterControls parameterControls) { throw notAllowed(); diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/BiArgExpressionProcessor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/BiArgExpressionProcessor.java new file mode 100644 index 000000000..807c4de32 --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/BiArgExpressionProcessor.java @@ -0,0 +1,10 @@ +package org.jbehave.core.expressions; + +import java.util.function.BiFunction; + +public class BiArgExpressionProcessor extends MultiArgExpressionProcessor { + + public BiArgExpressionProcessor(String expressionName, BiFunction evaluator) { + super(expressionName, 2, args -> evaluator.apply(args.get(0), args.get(1))); + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/DelegatingExpressionProcessor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/DelegatingExpressionProcessor.java new file mode 100644 index 000000000..d34de05fd --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/DelegatingExpressionProcessor.java @@ -0,0 +1,23 @@ +package org.jbehave.core.expressions; + +import java.util.Collection; +import java.util.Optional; + +public class DelegatingExpressionProcessor implements ExpressionProcessor { + + private final Collection> delegates; + + public DelegatingExpressionProcessor(Collection> delegates) { + this.delegates = delegates; + } + + @SuppressWarnings("unchecked") + @Override + public Optional execute(String expression) { + return (Optional) delegates.stream() + .map(processor -> processor.execute(expression)) + .filter(Optional::isPresent) + .findFirst() + .orElseGet(Optional::empty); + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionArguments.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionArguments.java new file mode 100644 index 000000000..d118734db --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionArguments.java @@ -0,0 +1,64 @@ +package org.jbehave.core.expressions; + +import java.util.List; + +import org.apache.commons.text.StringTokenizer; +import org.apache.commons.text.matcher.StringMatcher; +import org.apache.commons.text.matcher.StringMatcherFactory; + +class ExpressionArguments { + private static final char DELIMITER = ','; + private static final char ESCAPE = '\\'; + + private static final StringMatcher QUOTE_MATCHER = StringMatcherFactory.INSTANCE.stringMatcher("\"\"\""); + private static final StringMatcher TRIMMER_MATCHER = StringMatcherFactory.INSTANCE.trimMatcher(); + private static final IgnoredMatcher IGNORED_MATCHER = new IgnoredMatcher(); + + private final List arguments; + + ExpressionArguments(String argumentsAsString) { + this(argumentsAsString, Integer.MAX_VALUE); + } + + ExpressionArguments(String argumentsAsString, int argsLimit) { + StringTokenizer argumentsTokenizer = new StringTokenizer(argumentsAsString) + .setDelimiterMatcher(new DelimiterMatcher(argsLimit)) + .setQuoteMatcher(QUOTE_MATCHER) + .setTrimmerMatcher(TRIMMER_MATCHER) + .setIgnoredMatcher(IGNORED_MATCHER) + .setIgnoreEmptyTokens(false); + this.arguments = argumentsTokenizer.getTokenList(); + } + + public List getArguments() { + return arguments; + } + + private static final class DelimiterMatcher implements StringMatcher { + private final int resultsLimit; + private int totalNumberOfMatches = 1; + + private DelimiterMatcher(int resultsLimit) { + this.resultsLimit = resultsLimit; + } + + @Override + public int isMatch(char[] buffer, int start, int bufferStart, int bufferEnd) { + if (DELIMITER == buffer[start] && (start == 0 || ESCAPE != buffer[start - 1])) { + if (resultsLimit > totalNumberOfMatches) { + totalNumberOfMatches++; + return 1; + } + } + return 0; + } + } + + private static final class IgnoredMatcher implements StringMatcher { + @Override + public int isMatch(char[] buffer, int start, int bufferStart, int bufferEnd) { + int next = start + 1; + return ESCAPE == buffer[start] && next < buffer.length && DELIMITER == buffer[next] ? 1 : 0; + } + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionProcessor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionProcessor.java new file mode 100644 index 000000000..e468c2e04 --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionProcessor.java @@ -0,0 +1,7 @@ +package org.jbehave.core.expressions; + +import java.util.Optional; + +public interface ExpressionProcessor { + Optional execute(String expression); +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolver.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolver.java new file mode 100644 index 000000000..a06e8e423 --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolver.java @@ -0,0 +1,100 @@ +package org.jbehave.core.expressions; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ExpressionResolver { + + private static final Pattern GREEDY_EXPRESSION_PATTERN = Pattern.compile("#\\{((?:(?!#\\{|\\$\\{).)*)}", + Pattern.DOTALL); + private static final Pattern RELUCTANT_EXPRESSION_PATTERN = Pattern.compile( + "#\\{((?:(?![#{])[^)](?![()]))*?|(?:(?!#\\{|\\$\\{).)*?\\)|(?:(?!#\\{|\\$\\{).)*?)}", + Pattern.DOTALL); + private static final List PATTERNS = Arrays.asList(RELUCTANT_EXPRESSION_PATTERN, + GREEDY_EXPRESSION_PATTERN); + + private static final String REPLACEMENT_PATTERN = "\\#\\{%s\\}"; + + private final Set> expressionProcessors; + private final ExpressionResolverMonitor expressionResolverMonitor; + + public ExpressionResolver(Set> expressionProcessors, + ExpressionResolverMonitor expressionResolverMonitor) { + this.expressionProcessors = expressionProcessors; + this.expressionResolverMonitor = expressionResolverMonitor; + } + + /** + * Evaluates expressions including nested ones. + *
+ * Syntax: + *
+ * + * #{expression(arguments...)} + * #{expression(arguments..., #{expression(arguments...)})} + * #{expression(arguments..., #{expression})} + * + *
+ * Example: + *
+ * + * #{shiftDate("1942-12-02T01:23:40+04:00", "yyyy-MM-dd'T'HH:mm:ssz", "P43Y4M3W3D")} + *
+ * #{encodeToBase64(#{fromEpochSecond(-523641111)})} + *
+ * + * @param stringWithExpressions the string with expressions to evaluate + * @return the resulting string with expression placeholders replaced with expressions evaluation results + */ + public Object resolveExpressions(boolean dryRun, String stringWithExpressions) { + if (dryRun) { + return stringWithExpressions; + } + try { + return resolveExpressions(stringWithExpressions, PATTERNS.iterator()); + } catch (RuntimeException e) { + expressionResolverMonitor.onExpressionProcessingError(stringWithExpressions, e); + throw e; + } + } + + private Object resolveExpressions(String value, Iterator expressionPatterns) { + String processedValue = value; + Matcher expressionMatcher = expressionPatterns.next().matcher(processedValue); + boolean expressionFound = false; + while (expressionMatcher.find()) { + expressionFound = true; + String expression = expressionMatcher.group(1); + Object expressionResult = apply(expression); + if (!(expressionResult instanceof String) && ("#{" + expression + "}").equals(processedValue)) { + return expressionResult; + } + if (!expressionResult.equals(expression)) { + String regex = String.format(REPLACEMENT_PATTERN, Pattern.quote(expression)); + processedValue = processedValue.replaceFirst(regex, + Matcher.quoteReplacement(String.valueOf(expressionResult))); + expressionFound = false; + expressionMatcher.reset(processedValue); + } + } + if (expressionFound && expressionPatterns.hasNext()) { + return resolveExpressions(processedValue, expressionPatterns); + } + return processedValue; + } + + private Object apply(String expression) { + for (ExpressionProcessor processor : expressionProcessors) { + Optional optional = processor.execute(expression); + if (optional.isPresent()) { + return optional.get(); + } + } + return expression; + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolverMonitor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolverMonitor.java new file mode 100644 index 000000000..26cb427ba --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/ExpressionResolverMonitor.java @@ -0,0 +1,6 @@ +package org.jbehave.core.expressions; + +public interface ExpressionResolverMonitor { + + void onExpressionProcessingError(String stringWithExpressions, RuntimeException error); +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/MultiArgExpressionProcessor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/MultiArgExpressionProcessor.java new file mode 100644 index 000000000..33c5603c1 --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/MultiArgExpressionProcessor.java @@ -0,0 +1,91 @@ +package org.jbehave.core.expressions; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MultiArgExpressionProcessor implements ExpressionProcessor { + + private static final int ARGS_GROUP = 1; + + private final Pattern pattern; + private final String expressionName; + private final int minArgNumber; + private final int maxArgNumber; + private final Function argsParser; + private final Function, T> evaluator; + + public MultiArgExpressionProcessor(String expressionName, int minArgNumber, int maxArgNumber, + Function argsParser, Function, T> evaluator) { + this.pattern = Pattern.compile("^" + expressionName + "\\((.*)\\)$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + this.expressionName = expressionName; + this.minArgNumber = minArgNumber; + this.maxArgNumber = maxArgNumber; + this.argsParser = argsParser; + this.evaluator = evaluator; + } + + public MultiArgExpressionProcessor(String expressionName, int minArgNumber, int maxArgNumber, + Function, T> evaluator) { + this(expressionName, minArgNumber, maxArgNumber, ExpressionArguments::new, evaluator); + } + + public MultiArgExpressionProcessor(String expressionName, int expectedArgNumber, + Function, T> evaluator) { + this(expressionName, expectedArgNumber, expectedArgNumber, evaluator); + } + + public MultiArgExpressionProcessor(String expressionName, int expectedArgNumber, + Function argsParser, Function, T> evaluator) { + this(expressionName, expectedArgNumber, expectedArgNumber, argsParser, evaluator); + } + + @Override + public Optional execute(String expression) { + Matcher expressionMatcher = pattern.matcher(expression); + if (expressionMatcher.find()) { + List args = parseArgs(expressionMatcher.group(ARGS_GROUP)); + T expressionResult = evaluator.apply(args); + return Optional.of(expressionResult); + } + return Optional.empty(); + } + + private List parseArgs(String argsAsString) { + if (minArgNumber == 1 && maxArgNumber == 1) { + return Collections.singletonList(argsAsString); + } + List args = argsParser.apply(argsAsString).getArguments(); + int argsNumber = args.size(); + if (minArgNumber == maxArgNumber) { + if (argsNumber != minArgNumber) { + throwException(argsAsString, argsNumber, error -> error.append(minArgNumber)); + } + } else if (argsNumber < minArgNumber || argsNumber > maxArgNumber) { + throwException(argsAsString, argsNumber, error -> error.append("from ").append(minArgNumber).append(" to " + ).append(maxArgNumber)); + } + return args; + } + + private void throwException(String argsAsString, int argsNumber, Consumer expectationsAppender) { + StringBuilder errorMessageBuilder = new StringBuilder("The expected number of arguments for '") + .append(expressionName) + .append("' expression is "); + expectationsAppender.accept(errorMessageBuilder); + errorMessageBuilder.append(", but found ") + .append(argsNumber) + .append(" argument"); + if (argsNumber != 1) { + errorMessageBuilder.append('s'); + } + if (argsNumber > 0) { + errorMessageBuilder.append(": '").append(argsAsString).append('\''); + } + throw new IllegalArgumentException(errorMessageBuilder.toString()); + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/NullExpressionResolverMonitor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/NullExpressionResolverMonitor.java new file mode 100644 index 000000000..407ad298d --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/NullExpressionResolverMonitor.java @@ -0,0 +1,11 @@ +package org.jbehave.core.expressions; + +/** + * Null Object Pattern + * implementation of {@link ExpressionResolverMonitor}. Can be extended to override only the methods of interest. + */ +public class NullExpressionResolverMonitor implements ExpressionResolverMonitor { + @Override + public void onExpressionProcessingError(String stringWithExpressions, RuntimeException error) { + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/PrintStreamExpressionResolverMonitor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/PrintStreamExpressionResolverMonitor.java new file mode 100644 index 000000000..cb7927e8d --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/PrintStreamExpressionResolverMonitor.java @@ -0,0 +1,31 @@ +package org.jbehave.core.expressions; + +import java.io.PrintStream; + +import org.jbehave.core.reporters.Format; + +/** + * Monitor that reports to a {@link PrintStream}, defaulting to {@link System#out} + */ +public class PrintStreamExpressionResolverMonitor extends PrintingExpressionResolverMonitor { + + private PrintStream output; + + public PrintStreamExpressionResolverMonitor() { + this(System.out); + } + + public PrintStreamExpressionResolverMonitor(PrintStream output) { + this.output = output; + } + + @Override + protected void print(String format, Object... args) { + Format.println(output, format, args); + } + + @Override + protected void printStackTrace(Throwable e) { + e.printStackTrace(output); + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/PrintingExpressionResolverMonitor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/PrintingExpressionResolverMonitor.java new file mode 100644 index 000000000..520032d09 --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/PrintingExpressionResolverMonitor.java @@ -0,0 +1,17 @@ +package org.jbehave.core.expressions; + +/** + * Abstract monitor that reports to output which should be defined in child implementations. + */ +public abstract class PrintingExpressionResolverMonitor implements ExpressionResolverMonitor { + + @Override + public void onExpressionProcessingError(String stringWithExpressions, RuntimeException error) { + print("Unable to process expression(s) '%s'", stringWithExpressions); + printStackTrace(error); + } + + protected abstract void print(String format, Object... args); + + protected abstract void printStackTrace(Throwable e); +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/RelaxedMultiArgExpressionProcessor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/RelaxedMultiArgExpressionProcessor.java new file mode 100644 index 000000000..9c3ac85b6 --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/RelaxedMultiArgExpressionProcessor.java @@ -0,0 +1,19 @@ +package org.jbehave.core.expressions; + +import java.util.List; +import java.util.function.Function; + +public class RelaxedMultiArgExpressionProcessor extends MultiArgExpressionProcessor { + + public RelaxedMultiArgExpressionProcessor(String expressionName, int argsLimit, + Function, T> transformer) { + super(expressionName, argsLimit, argumentsAsString -> new ExpressionArguments(argumentsAsString, argsLimit), + transformer); + } + + public RelaxedMultiArgExpressionProcessor(String expressionName, int minArgNumber, int argsLimit, + Function, T> transformer) { + super(expressionName, minArgNumber, argsLimit, + argumentsAsString -> new ExpressionArguments(argumentsAsString, argsLimit), transformer); + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/expressions/SingleArgExpressionProcessor.java b/jbehave-core/src/main/java/org/jbehave/core/expressions/SingleArgExpressionProcessor.java new file mode 100644 index 000000000..7a1cab0c0 --- /dev/null +++ b/jbehave-core/src/main/java/org/jbehave/core/expressions/SingleArgExpressionProcessor.java @@ -0,0 +1,10 @@ +package org.jbehave.core.expressions; + +import java.util.function.Function; + +public class SingleArgExpressionProcessor extends MultiArgExpressionProcessor { + + public SingleArgExpressionProcessor(String functionName, Function evaluator) { + super(functionName, 1, args -> evaluator.apply(args.get(0))); + } +} diff --git a/jbehave-core/src/main/java/org/jbehave/core/steps/AbstractCandidateSteps.java b/jbehave-core/src/main/java/org/jbehave/core/steps/AbstractCandidateSteps.java index a9811d613..159736e64 100644 --- a/jbehave-core/src/main/java/org/jbehave/core/steps/AbstractCandidateSteps.java +++ b/jbehave-core/src/main/java/org/jbehave/core/steps/AbstractCandidateSteps.java @@ -1,8 +1,11 @@ package org.jbehave.core.steps; import java.lang.reflect.Method; +import java.util.List; import org.jbehave.core.configuration.Configuration; +import org.jbehave.core.parsers.StepMatcher; +import org.jbehave.core.parsers.StepPatternParser; public abstract class AbstractCandidateSteps implements CandidateSteps { private final Configuration configuration; @@ -15,14 +18,28 @@ protected Configuration configuration() { return configuration; } - protected StepCandidate createCandidate(String stepPatternAsString, int priority, StepType stepType, Method method, - Class type, InjectableStepsFactory stepsFactory) { - StepCandidate candidate = new StepCandidate(stepPatternAsString, priority, stepType, method, type, - stepsFactory, configuration.stepsContext(), configuration.keywords(), configuration.stepPatternParser(), - configuration.parameterConverters(), configuration.parameterControls()); - candidate.useStepMonitor(configuration.stepMonitor()); - candidate.useParanamer(configuration.paranamer()); - candidate.doDryRun(configuration.storyControls().dryRun()); - return candidate; + protected void addCandidatesFromVariants(List candidates, Method method, StepType stepType, + String value, int priority, Class type, InjectableStepsFactory stepsFactory, String[] steps) { + StepPatternParser stepPatternParser = configuration.stepPatternParser(); + PatternVariantBuilder patternVariantBuilder = new PatternVariantBuilder(value); + for (String variant : patternVariantBuilder.allVariants()) { + StepMatcher stepMatcher = stepPatternParser.parseStep(stepType, variant); + StepCreator stepCreator = createStepCreator(type, stepsFactory, stepMatcher); + stepCreator.useParanamer(configuration.paranamer()); + StepCandidate candidate = new StepCandidate(variant, priority, stepType, method, type, stepsFactory, + configuration.keywords(), stepMatcher, stepPatternParser.getPrefix(), stepCreator, steps, + configuration.stepMonitor()); + candidates.add(candidate); + } + } + + protected final StepCreator createStepCreator(Class type, InjectableStepsFactory stepsFactory) { + return createStepCreator(type, stepsFactory, null); + } + + private StepCreator createStepCreator(Class type, InjectableStepsFactory stepsFactory, StepMatcher stepMatcher) { + return new StepCreator(type, stepsFactory, configuration.stepsContext(), configuration.parameterConverters(), + configuration.expressionResolver(), configuration.parameterControls(), stepMatcher, + configuration.stepMonitor(), configuration.storyControls().dryRun()); } } diff --git a/jbehave-core/src/main/java/org/jbehave/core/steps/BeforeOrAfterStep.java b/jbehave-core/src/main/java/org/jbehave/core/steps/BeforeOrAfterStep.java index 8acb2e1fd..058ae799c 100755 --- a/jbehave-core/src/main/java/org/jbehave/core/steps/BeforeOrAfterStep.java +++ b/jbehave-core/src/main/java/org/jbehave/core/steps/BeforeOrAfterStep.java @@ -24,11 +24,6 @@ public class BeforeOrAfterStep { private final int order; private final StepCreator stepCreator; private final Outcome outcome; - private StepMonitor stepMonitor = new SilentStepMonitor(); - - public BeforeOrAfterStep(Method method, int order, StepCreator stepCreator) { - this(method, order, Outcome.ANY, stepCreator); - } public BeforeOrAfterStep(Method method, int order, Outcome outcome, StepCreator stepCreator) { this.method = method; @@ -57,14 +52,12 @@ public Step createStepUponOutcome(Meta storyAndScenarioMeta) { return stepCreator.createAfterStepUponOutcome(method, outcome, storyAndScenarioMeta); } - public void useStepMonitor(StepMonitor stepMonitor) { - this.stepMonitor = stepMonitor; - this.stepCreator.useStepMonitor(stepMonitor); - } - @Override public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(method).append(order).append(outcome) - .append(stepMonitor).toString(); + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append(method) + .append(order) + .append(outcome) + .toString(); } } diff --git a/jbehave-core/src/main/java/org/jbehave/core/steps/CompositeCandidateSteps.java b/jbehave-core/src/main/java/org/jbehave/core/steps/CompositeCandidateSteps.java index ca07bdb93..5e5ef8ea8 100644 --- a/jbehave-core/src/main/java/org/jbehave/core/steps/CompositeCandidateSteps.java +++ b/jbehave-core/src/main/java/org/jbehave/core/steps/CompositeCandidateSteps.java @@ -36,18 +36,8 @@ public List listCandidates() { private void addCandidatesFromComposites(List candidates, List composites) { for (Composite composite : composites) { String[] steps = composite.getSteps().toArray(new String[0]); - addCandidatesFromVariants(candidates, composite.getStepType(), composite.getStepWithoutStartingWord(), - composite.getPriority(), steps); - } - } - - private void addCandidatesFromVariants(List candidates, StepType stepType, String value, - int priority, String[] steps) { - PatternVariantBuilder b = new PatternVariantBuilder(value); - for (String variant : b.allVariants()) { - StepCandidate candidate = createCandidate(variant, priority, stepType, null, null, null); - candidate.composedOf(steps); - candidates.add(candidate); + addCandidatesFromVariants(candidates, null, composite.getStepType(), + composite.getStepWithoutStartingWord(), composite.getPriority(), null, null, steps); } } diff --git a/jbehave-core/src/main/java/org/jbehave/core/steps/ConditionalStepCandidate.java b/jbehave-core/src/main/java/org/jbehave/core/steps/ConditionalStepCandidate.java index bf62ea5ee..221f7bebe 100644 --- a/jbehave-core/src/main/java/org/jbehave/core/steps/ConditionalStepCandidate.java +++ b/jbehave-core/src/main/java/org/jbehave/core/steps/ConditionalStepCandidate.java @@ -17,9 +17,10 @@ public class ConditionalStepCandidate extends StepCandidate { private ConditionalStepCandidate(String patternAsString, int priority, StepType stepType, Map stepCreators, Keywords keywords, StepMatcher stepMatcher, - String parameterPrefixString, StepCreator stepCreator, StepConditionMatcher stepConditionMatcher) { + String parameterPrefixString, StepCreator stepCreator, StepConditionMatcher stepConditionMatcher, + StepMonitor stepMonitor) { super(patternAsString, priority, stepType, null, null, null, keywords, stepMatcher, parameterPrefixString, - stepCreator); + stepCreator, null, stepMonitor); this.stepCreators = stepCreators; this.stepConditionMatcher = stepConditionMatcher; } @@ -46,11 +47,9 @@ public static StepCandidate from(StepConditionMatcher stepConditionMatcher, StepCandidate baseCandidate = conditionalCandidates.get(0); - StepCandidate candidate = new ConditionalStepCandidate(baseCandidate.getPatternAsString(), - baseCandidate.getPriority(), baseCandidate.getStepType(), stepCreators, - baseCandidate.getKeywords(), baseCandidate.getStepMatcher(), - baseCandidate.getParameterPrefix(), baseCandidate.getStepCreator(), stepConditionMatcher); - candidate.useStepMonitor(baseCandidate.getStepMonitor()); - return candidate; + return new ConditionalStepCandidate(baseCandidate.getPatternAsString(), baseCandidate.getPriority(), + baseCandidate.getStepType(), stepCreators, baseCandidate.getKeywords(), + baseCandidate.getStepMatcher(), baseCandidate.getParameterPrefix(), baseCandidate.getStepCreator(), + stepConditionMatcher, baseCandidate.getStepMonitor()); } } diff --git a/jbehave-core/src/main/java/org/jbehave/core/steps/StepCandidate.java b/jbehave-core/src/main/java/org/jbehave/core/steps/StepCandidate.java index ad2d2cde4..98c3e10f3 100755 --- a/jbehave-core/src/main/java/org/jbehave/core/steps/StepCandidate.java +++ b/jbehave-core/src/main/java/org/jbehave/core/steps/StepCandidate.java @@ -19,8 +19,6 @@ import org.jbehave.core.configuration.Keywords; import org.jbehave.core.configuration.Keywords.StartingWordNotFound; import org.jbehave.core.parsers.StepMatcher; -import org.jbehave.core.parsers.StepPatternParser; -import org.jbehave.core.steps.context.StepsContext; /** * A StepCandidate is associated to a Java method annotated with {@link Given}, @@ -43,30 +41,12 @@ public class StepCandidate { private final StepMatcher stepMatcher; private final StepCreator stepCreator; private final String parameterPrefix; - private String[] composedSteps; - private StepMonitor stepMonitor = new SilentStepMonitor(); - - public StepCandidate(String patternAsString, int priority, StepType stepType, Method method, Class stepsType, - InjectableStepsFactory stepsFactory, StepsContext stepsContext, Keywords keywords, - StepPatternParser stepPatternParser, ParameterConverters parameterConverters, - ParameterControls parameterControls) { - this(patternAsString, priority, stepType, method, stepsType, stepsFactory, stepsContext, keywords, - stepPatternParser.parseStep(stepType, patternAsString), stepPatternParser.getPrefix(), - parameterConverters, parameterControls); - } - - private StepCandidate(String patternAsString, int priority, StepType stepType, Method method, Class stepsType, - InjectableStepsFactory stepsFactory, StepsContext stepsContext, Keywords keywords, - StepMatcher stepMatcher, String parameterPrefixString, ParameterConverters parameterConverters, - ParameterControls parameterControls) { - this(patternAsString, priority, stepType, method, stepsType, stepsFactory, keywords, stepMatcher, - parameterPrefixString, new StepCreator(stepsType, stepsFactory, stepsContext, parameterConverters, - parameterControls, stepMatcher, new SilentStepMonitor())); - } + private final String[] composedSteps; + private StepMonitor stepMonitor; public StepCandidate(String patternAsString, int priority, StepType stepType, Method method, Class stepsType, InjectableStepsFactory stepsFactory, Keywords keywords, StepMatcher stepMatcher, - String parameterPrefixString, StepCreator stepCreator) { + String parameterPrefixString, StepCreator stepCreator, String[] steps, StepMonitor stepMonitor) { this.patternAsString = patternAsString; this.priority = priority; this.stepType = stepType; @@ -77,6 +57,8 @@ public StepCandidate(String patternAsString, int priority, StepType stepType, Me this.stepMatcher = stepMatcher; this.stepCreator = stepCreator; this.parameterPrefix = parameterPrefixString; + this.composedSteps = steps; + this.stepMonitor = stepMonitor; } public Method getMethod() { @@ -120,18 +102,10 @@ public void useStepMonitor(StepMonitor stepMonitor) { this.stepCreator.useStepMonitor(stepMonitor); } - public void doDryRun(boolean dryRun) { - this.stepCreator.doDryRun(dryRun); - } - public void useParanamer(Paranamer paranamer) { this.stepCreator.useParanamer(paranamer); } - public void composedOf(String[] steps) { - this.composedSteps = steps; - } - public boolean isComposite() { return composedSteps != null && composedSteps.length > 0; } diff --git a/jbehave-core/src/main/java/org/jbehave/core/steps/StepCreator.java b/jbehave-core/src/main/java/org/jbehave/core/steps/StepCreator.java index e8064c9ce..95b9e8a99 100755 --- a/jbehave-core/src/main/java/org/jbehave/core/steps/StepCreator.java +++ b/jbehave-core/src/main/java/org/jbehave/core/steps/StepCreator.java @@ -26,6 +26,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.thoughtworks.paranamer.NullParanamer; import com.thoughtworks.paranamer.Paranamer; @@ -40,6 +41,7 @@ import org.jbehave.core.condition.StepConditionMatchException; import org.jbehave.core.condition.StepConditionMatcher; import org.jbehave.core.configuration.Keywords; +import org.jbehave.core.expressions.ExpressionResolver; import org.jbehave.core.failures.BeforeOrAfterFailed; import org.jbehave.core.failures.IgnoringStepsFailure; import org.jbehave.core.failures.RestartingScenarioFailure; @@ -69,26 +71,29 @@ public class StepCreator { private final Class stepsType; private final InjectableStepsFactory stepsFactory; private final ParameterConverters parameterConverters; + private final ExpressionResolver expressionResolver; private final ParameterControls parameterControls; private final Pattern delimitedNamePattern; private final StepMatcher stepMatcher; private final StepsContext stepsContext; + private final boolean dryRun; private StepMonitor stepMonitor; private Paranamer paranamer = new NullParanamer(); - private boolean dryRun = false; - public StepCreator(Class stepsType, InjectableStepsFactory stepsFactory, - StepsContext stepsContext, ParameterConverters parameterConverters, ParameterControls parameterControls, - StepMatcher stepMatcher, StepMonitor stepMonitor) { + public StepCreator(Class stepsType, InjectableStepsFactory stepsFactory, StepsContext stepsContext, + ParameterConverters parameterConverters, ExpressionResolver expressionResolver, + ParameterControls parameterControls, StepMatcher stepMatcher, StepMonitor stepMonitor, boolean dryRun) { this.stepsType = stepsType; this.stepsFactory = stepsFactory; this.stepsContext = stepsContext; this.parameterConverters = parameterConverters; + this.expressionResolver = expressionResolver; this.parameterControls = parameterControls; this.stepMatcher = stepMatcher; this.stepMonitor = stepMonitor; this.delimitedNamePattern = Pattern.compile(parameterControls.nameDelimiterLeft() + "([\\w\\-\\h.]+?)" + parameterControls.nameDelimiterRight(), Pattern.DOTALL); + this.dryRun = dryRun; } public void useStepMonitor(StepMonitor stepMonitor) { @@ -99,10 +104,6 @@ public void useParanamer(Paranamer paranamer) { this.paranamer = paranamer; } - public void doDryRun(boolean dryRun) { - this.dryRun = dryRun; - } - public Object stepsInstance() { return stepsFactory.createInstanceOfType(stepsType); } @@ -397,18 +398,6 @@ private String[] parameterValuesForStep(Matcher matcher, Map nam return parameters; } - private Object[] convertParameterValues(String[] valuesAsString, Type[] types, ParameterName[] names) { - final Object[] parameters = new Object[valuesAsString.length]; - for (int position = 0; position < valuesAsString.length; position++) { - if (names[position].fromContext) { - parameters[position] = stepsContext.get(valuesAsString[position]); - } else { - parameters[position] = parameterConverters.convert(valuesAsString[position], types[position]); - } - } - return parameters; - } - private String parameterForPosition(Matcher matcher, int position, ParameterName[] names, Map namedParameters, boolean overrideWithTableParameters) { int namePosition = parameterPosition(names, position); @@ -874,7 +863,6 @@ private String getStepName() { } public class ParametrisedStep extends ReportingAbstractStep { - private Object[] convertedParameters; private String parametrisedStep; private final Method method; private final String stepWithoutStartingWord; @@ -900,7 +888,7 @@ public StepResult perform(UUIDExceptionWrapper storyFailure) { String stepAsString = getStepAsString(); Timer timer = new Timer().start(); try { - parametriseStep(); + Object[] convertedParameters = parametriseStep(); stepMonitor.beforePerforming(parametrisedStep, dryRun, method); if (!dryRun && method != null) { Object outputObject = method.invoke(stepsInstance(), convertedParameters); @@ -953,24 +941,43 @@ public String asString(Keywords keywords) { return parametrisedStep; } - private void parametriseStep() { + private Object[] parametriseStep() { Matcher matcher = stepMatcher.matcher(stepWithoutStartingWord); matcher.find(); ParameterName[] names = parameterNames(method); Type[] types = parameterTypes(method, names); String[] parameterValues = parameterValuesForStep(matcher, namedParameters, types, names, true); - convertedParameters = method == null ? parameterValues - : convertParameterValues(parameterValues, types, names); - addNamedParametersToExamplesTables(); + Object[] convertedParameters; + if (method == null) { + convertedParameters = parameterValues; + } else { + convertedParameters = convertParameterValues(parameterValues, types, names); + } + addNamedParametersToExamplesTables(convertedParameters); parametrisedStep = parametrisedStep(getStepAsString(), namedParameters, types, parameterValues); - } - - private void addNamedParametersToExamplesTables() { - for (Object object : convertedParameters) { - if (object instanceof ExamplesTable) { - ((ExamplesTable) object).withNamedParameters(namedParameters); + return convertedParameters; + } + + private Object[] convertParameterValues(String[] parameterValues, Type[] types, ParameterName[] names) { + final Object[] parameters = new Object[parameterValues.length]; + for (int i = 0; i < parameterValues.length; i++) { + String parameterValue = parameterValues[i]; + if (names[i].fromContext) { + parameters[i] = stepsContext.get(parameterValue); + } else { + String expressionEvaluationResult = parameterValue != null + ? String.valueOf(expressionResolver.resolveExpressions(dryRun, parameterValue)) : null; + parameters[i] = parameterConverters.convert(expressionEvaluationResult, types[i]); } } + return parameters; + } + + private void addNamedParametersToExamplesTables(Object[] convertedParameters) { + Stream.of(convertedParameters) + .filter(ExamplesTable.class::isInstance) + .map(ExamplesTable.class::cast) + .forEach(examplesTable -> examplesTable.withNamedParameters(namedParameters)); } } diff --git a/jbehave-core/src/main/java/org/jbehave/core/steps/Steps.java b/jbehave-core/src/main/java/org/jbehave/core/steps/Steps.java index d99294bd6..abbb37fb2 100644 --- a/jbehave-core/src/main/java/org/jbehave/core/steps/Steps.java +++ b/jbehave-core/src/main/java/org/jbehave/core/steps/Steps.java @@ -5,8 +5,6 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.jbehave.core.annotations.AfterScenario.Outcome.ANY; -import static org.jbehave.core.annotations.AfterScenario.Outcome.FAILURE; -import static org.jbehave.core.annotations.AfterScenario.Outcome.SUCCESS; import static org.jbehave.core.steps.StepType.GIVEN; import static org.jbehave.core.steps.StepType.THEN; import static org.jbehave.core.steps.StepType.WHEN; @@ -20,10 +18,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.ToIntFunction; -import java.util.stream.Collectors; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -110,7 +108,6 @@ public class Steps extends AbstractCandidateSteps { private final Class type; private final InjectableStepsFactory stepsFactory; - private final StepCreator stepCreator; /** * Creates Steps with default configuration for a class extending this @@ -130,15 +127,12 @@ public Steps(Configuration configuration) { super(configuration); this.type = this.getClass(); this.stepsFactory = new InstanceStepsFactory(configuration, this); - stepCreator = new StepCreator(type, stepsFactory, configuration().stepsContext(), - configuration().parameterConverters(), configuration().parameterControls(), null, - configuration().stepMonitor()); } /** * Creates Steps with given custom configuration and a steps instance * containing the candidate step methods - * + * * @param configuration the Configuration * @param instance the steps instance */ @@ -150,7 +144,7 @@ public Steps(Configuration configuration, Object instance) { * Creates Steps with given custom configuration and a steps instance type * containing the candidate step methods. The steps instance is created * using the steps instance factory provided. - * + * * @param configuration the Configuration * @param type the steps instance type * @param stepsFactory the {@link InjectableStepsFactory} @@ -159,9 +153,6 @@ public Steps(Configuration configuration, Class type, InjectableStepsFactory super(configuration); this.type = type; this.stepsFactory = stepsFactory; - stepCreator = new StepCreator(type, stepsFactory, configuration().stepsContext(), - configuration().parameterConverters(), configuration().parameterControls(), null, - configuration().stepMonitor()); } public Class type() { @@ -234,10 +225,9 @@ private Map> findAliases() { private void addCandidatesFromVariants(List candidates, Method method, StepType stepType, String value, int priority) { - PatternVariantBuilder b = new PatternVariantBuilder(value); - for (String variant : b.allVariants()) { - addCandidate(candidates, method, stepType, variant, priority); - } + String[] composedSteps = method.isAnnotationPresent(Composite.class) + ? method.getAnnotation(Composite.class).steps() : new String[0]; + addCandidatesFromVariants(candidates, method, stepType, value, priority, type, stepsFactory, composedSteps); } private void addCandidatesFromAliases(List candidates, Method method, StepType stepType, @@ -254,79 +244,69 @@ private void addCandidatesFromAliases(List candidates, Method met } } - private void addCandidate(List candidates, Method method, StepType stepType, - String stepPatternAsString, int priority) { - StepCandidate candidate = createCandidate(stepPatternAsString, priority, stepType, method, type, stepsFactory); - if (method.isAnnotationPresent(Composite.class)) { - candidate.composedOf(method.getAnnotation(Composite.class).steps()); - } - candidates.add(candidate); - } - @Override public List listBeforeStories() { - return listSteps(BeforeStories.class, v -> true, BeforeStories::order); + return listSteps(BeforeStories.class, a -> true, BeforeStories::order); } @Override public List listAfterStories() { - return listSteps(AfterStories.class, v -> true, AfterStories::order); + return listSteps(AfterStories.class, a -> true, AfterStories::order); } @Override public List listBeforeStory(boolean givenStory) { - return listSteps(BeforeStory.class, v -> v.uponGivenStory() == givenStory, BeforeStory::order); + return listSteps(BeforeStory.class, a -> a.uponGivenStory() == givenStory, BeforeStory::order); } @Override public List listAfterStory(boolean givenStory) { - return listSteps(AfterStory.class, v -> v.uponGivenStory() == givenStory, AfterStory::order); + return listSteps(AfterStory.class, a -> a.uponGivenStory() == givenStory, AfterStory::order); } @Override public Map> listBeforeScenario() { - Map beforeScenarioMethods = methodsAnnotatedWith(BeforeScenario.class); - Map> stepsPerType = new EnumMap<>(ScenarioType.class); - for (ScenarioType scenarioType : ScenarioType.values()) { - stepsPerType.put(scenarioType, - listSteps(beforeScenarioMethods, v -> v.uponType() == scenarioType, BeforeScenario::order)); - } - return stepsPerType; + return listBeforeOrAfterScenarioSteps(BeforeScenario.class, (a, scenarioType) -> a.uponType() == scenarioType, + BeforeScenario::order, a -> ANY); } @Override public Map> listAfterScenario() { - Map afterScenarioMethods = methodsAnnotatedWith(AfterScenario.class); + return listBeforeOrAfterScenarioSteps(AfterScenario.class, (a, scenarioType) -> a.uponType() == scenarioType, + AfterScenario::order, AfterScenario::uponOutcome); + } + + public Map> listBeforeOrAfterScenarioSteps( + Class annotationClass, BiPredicate predicate, ToIntFunction order, + Function outcome) { + StepCreator stepCreator = createStepCreator(type, stepsFactory); + Map methods = methodsAnnotatedWith(annotationClass); Map> stepsPerType = new EnumMap<>(ScenarioType.class); for (ScenarioType scenarioType : ScenarioType.values()) { - List steps = new ArrayList<>(); - for (Outcome outcome : new Outcome[] { ANY, SUCCESS, FAILURE }) { - steps.addAll(listSteps(afterScenarioMethods, - v -> v.uponType() == scenarioType && v.uponOutcome() == outcome, - (m, a) -> new BeforeOrAfterStep(m, a.order(), outcome, stepCreator))); - } - stepsPerType.put(scenarioType, steps); + stepsPerType.put(scenarioType, listSteps(methods, a -> predicate.test(a, scenarioType), order, outcome, + stepCreator)); } return stepsPerType; } - private List listSteps(Class type, Predicate predicate, + private List listSteps(Class annotationClass, Predicate predicate, ToIntFunction order) { - return listSteps(methodsAnnotatedWith(type), predicate, order); + StepCreator stepCreator = createStepCreator(type, stepsFactory); + return listSteps(methodsAnnotatedWith(annotationClass), predicate, order, a -> ANY, stepCreator); } private List listSteps(Map methods, Predicate predicate, - ToIntFunction order) { - return listSteps(methods, predicate, (m, a) -> new BeforeOrAfterStep(m, order.applyAsInt(a), stepCreator)); - } - - private List listSteps(Map methods, - Predicate predicate, BiFunction factory) { + ToIntFunction order, Function outcome, StepCreator stepCreator) { return methods.entrySet() .stream() .filter(e -> predicate.test(e.getValue())) - .map(e -> factory.apply(e.getKey(), e.getValue())) - .collect(Collectors.toList()); + .map(e -> { + Method method = e.getKey(); + T annotation = e.getValue(); + return new BeforeOrAfterStep(method, order.applyAsInt(annotation), outcome.apply(annotation), + stepCreator); + }) + .collect(toList()); } private Method[] allMethods() { diff --git a/jbehave-core/src/test/java/org/jbehave/core/embedder/ExpressionResolverBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/embedder/ExpressionResolverBehaviour.java new file mode 100644 index 000000000..791952c69 --- /dev/null +++ b/jbehave-core/src/test/java/org/jbehave/core/embedder/ExpressionResolverBehaviour.java @@ -0,0 +1,267 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jbehave.core.embedder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.jbehave.core.expressions.DelegatingExpressionProcessor; +import org.jbehave.core.expressions.ExpressionProcessor; +import org.jbehave.core.expressions.ExpressionResolver; +import org.jbehave.core.expressions.PrintStreamExpressionResolverMonitor; +import org.jbehave.core.expressions.SingleArgExpressionProcessor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ExpressionResolverBehaviour { + + private static final String EXPRESSION_FORMAT = "#{%s}"; + private static final String EXPRESSION_KEYWORD = "target"; + private static final String EXPRESSION_KEYWORD_WITH_SEPARATOR = "tar\nget"; + private static final String EXPRESSION_RESULT = "target result with \\ and $"; + private static final String EXPRESSION_TRIM = "trim"; + private static final String EXPRESSION_CAPITALIZE = "capitalize"; + private static final String EXPRESSION_LOWER_CASE = "toLowerCase"; + private static final String UNSUPPORTED_EXPRESSION_KEYWORD = "unsupported"; + private static final String UNSUPPORTED_EXPRESSION = String.format(EXPRESSION_FORMAT, + UNSUPPORTED_EXPRESSION_KEYWORD); + + @Mock private ExpressionProcessor targetProcessor; + @Mock private ExpressionProcessor anotherProcessor; + + @ParameterizedTest + @CsvSource({ + "'target', '#{target}', %s, " + EXPRESSION_RESULT, + "'target', '{#{target}}', {%s}, " + EXPRESSION_RESULT, + "'target', '{#{target} and #{target}}', {%1$s and %1$s}, " + EXPRESSION_RESULT, + "'target(})', '#{target(})}', %s, " + EXPRESSION_RESULT, + "'tar\nget', '#{tar\nget}', %s, " + EXPRESSION_RESULT, + "'expr(value{1})', '#{expr(#{expr(#{expr(value{1})})})}', %s, value{1}", + "'generateDate(-P19Y, yyyy)', '{#{generateDate(-P19Y, yyyy)}\n}', '{%s\n}', " + EXPRESSION_RESULT + }) + void testSupportedExpression(String expressionKeyword, String input, String outputFormat, String outputValue) { + lenient().when(targetProcessor.execute(EXPRESSION_KEYWORD)).thenReturn(Optional.of(EXPRESSION_RESULT)); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + when(targetProcessor.execute(expressionKeyword)).thenReturn(Optional.of(outputValue)); + Object actual = expressionResolver.resolveExpressions(false, input); + String output = String.format(outputFormat, outputValue); + assertEquals(output, actual); + } + + @Test + void testSupportedExpressionNestedExpr() { + String input = "#{capitalize(#{trim(#{toLowerCase( VIVIDUS )})})}"; + String output = "Vividus"; + ExpressionProcessor processor = new DelegatingExpressionProcessor(Arrays.asList( + new SingleArgExpressionProcessor<>(EXPRESSION_TRIM, StringUtils::trim), + new SingleArgExpressionProcessor<>(EXPRESSION_LOWER_CASE, StringUtils::lowerCase), + new SingleArgExpressionProcessor<>(EXPRESSION_CAPITALIZE, StringUtils::capitalize) + )); + ExpressionResolver expressionResolver = new ExpressionResolver(Collections.singleton(processor), + new PrintStreamExpressionResolverMonitor()); + Object actual = expressionResolver.resolveExpressions(false, input); + assertEquals(output, actual); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @ValueSource(strings = { EXPRESSION_KEYWORD, "targets" }) + void testNestedExpressionWithoutParams(String nestedExpression) { + String input = String.format("#{capitalize(#{trim(#{%s})})}", nestedExpression); + String output = "Quetzalcoatlus"; + + SingleArgExpressionProcessor expressionProcessor = mock(); + when(expressionProcessor.execute(nestedExpression)).thenReturn(Optional.of(" quetzalcoatlus ")); + ExpressionProcessor processor = new DelegatingExpressionProcessor(Arrays.asList( + new SingleArgExpressionProcessor<>(EXPRESSION_TRIM, StringUtils::trim), + new SingleArgExpressionProcessor<>(EXPRESSION_CAPITALIZE, StringUtils::capitalize), + expressionProcessor + )); + ExpressionResolver expressionResolver = new ExpressionResolver(Collections.singleton(processor), + new PrintStreamExpressionResolverMonitor()); + Object actual = expressionResolver.resolveExpressions(false, input); + assertEquals(output, actual); + } + + @Test + void testUnsupportedExpression() { + when(anotherProcessor.execute(UNSUPPORTED_EXPRESSION_KEYWORD)).thenReturn(Optional.empty()); + when(targetProcessor.execute(UNSUPPORTED_EXPRESSION_KEYWORD)).thenReturn(Optional.empty()); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + Object actual = expressionResolver.resolveExpressions(false, UNSUPPORTED_EXPRESSION); + assertEquals(UNSUPPORTED_EXPRESSION, actual, "Unsupported expression, should leave as is"); + + verify(targetProcessor, times(2)).execute(UNSUPPORTED_EXPRESSION_KEYWORD); + verify(anotherProcessor, times(2)).execute(UNSUPPORTED_EXPRESSION_KEYWORD); + } + + @ParameterizedTest + @CsvSource({"${var}", "'#expr'", "{expr}", "value"}) + void testNonExpression(String nonExpression) { + ExpressionResolver expressionResolver = new ExpressionResolver(Collections.emptySet(), + new PrintStreamExpressionResolverMonitor()); + Object actual = expressionResolver.resolveExpressions(false, nonExpression); + assertEquals(nonExpression, actual, "Not expression, should leave as is"); + + verify(targetProcessor, never()).execute(nonExpression); + verify(anotherProcessor, never()).execute(nonExpression); + } + + @Test + void testValuesInExampleTable() { + when(targetProcessor.execute(EXPRESSION_KEYWORD)).thenReturn(Optional.of(EXPRESSION_RESULT)); + when(targetProcessor.execute(EXPRESSION_KEYWORD_WITH_SEPARATOR)) + .thenReturn(Optional.of(EXPRESSION_RESULT)); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + String header = "|value1|value2|value3|value4|\n"; + String inputTable = header + "|#{target}|simple|#{target}|#{tar\nget}|\n|#{target (something inside#$)}|simple" + + "|#{target}|#{tar\nget}|"; + String expectedTable = header + "|target result with \\ and $|simple|target result with \\ and $|target result" + + " with \\ and $|\n|target result with \\ and $|simple|target result with \\ and $|target result with" + + " \\ and $|"; + when(targetProcessor.execute("target (something inside#$)")) + .thenReturn(Optional.of(EXPRESSION_RESULT)); + Object actualTable = expressionResolver.resolveExpressions(false, inputTable); + assertEquals(expectedTable, actualTable); + verify(targetProcessor, times(6)).execute(anyString()); + } + + @Test + void testUnsupportedValuesInExampleTable() { + when(anotherProcessor.execute(UNSUPPORTED_EXPRESSION_KEYWORD)).thenReturn(Optional.empty()); + when(targetProcessor.execute(UNSUPPORTED_EXPRESSION_KEYWORD)).thenReturn(Optional.empty()); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + String inputTable = "|value1|value2|value3|\n|#{unsupported}|simple|#{unsupported}|"; + Object actualTable = expressionResolver.resolveExpressions(false, inputTable); + assertEquals(inputTable, actualTable); + } + + @Test + void testMixedValuesInExampleTable() { + when(anotherProcessor.execute(UNSUPPORTED_EXPRESSION_KEYWORD)).thenReturn(Optional.empty()); + when(targetProcessor.execute(UNSUPPORTED_EXPRESSION_KEYWORD)).thenReturn(Optional.empty()); + when(targetProcessor.execute(EXPRESSION_KEYWORD)).thenReturn(Optional.of(EXPRESSION_RESULT)); + when(targetProcessor.execute(EXPRESSION_KEYWORD_WITH_SEPARATOR)) + .thenReturn(Optional.of(EXPRESSION_RESULT)); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + String anotherExpressionKeyword = "another"; + when(anotherProcessor.execute(anotherExpressionKeyword)).thenReturn(Optional.of("another result")); + + String header = "|value1|value2|value3|value4|value5|\n"; + String inputTable = header + "|#{unsupported}|simple|#{target}|#{tar\nget}|#{another}|"; + String expectedTable = header + "|#{unsupported}|simple|target result with \\ and $|target result with \\ and" + + " $|another result|"; + Object actualTable = expressionResolver.resolveExpressions(false, inputTable); + assertEquals(expectedTable, actualTable); + } + + @Test + void testMixedExpressionsAndVariablesInExampleTable() { + when(targetProcessor.execute(EXPRESSION_KEYWORD)).thenReturn(Optional.of(EXPRESSION_RESULT)); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + String inputTable = "|value1|value2|\n|#{target}|${variable}|"; + String expectedTable = "|value1|value2|\n|target result with \\ and $|${variable}|"; + Object actualTable = expressionResolver.resolveExpressions(false, inputTable); + assertEquals(expectedTable, actualTable); + } + + @Test + void testExpressionProcessingError() { + String input = "#{exp(any)}"; + RuntimeException exception = new RuntimeException(); + ExpressionProcessor processor = new SingleArgExpressionProcessor<>("exp", value -> { + throw exception; + }); + PrintStream printStream = mock(); + ExpressionResolver expressionResolver = new ExpressionResolver(Collections.singleton(processor), + new PrintStreamExpressionResolverMonitor(printStream)); + RuntimeException actualException = assertThrows(RuntimeException.class, + () -> expressionResolver.resolveExpressions(false, input)); + assertEquals(exception, actualException); + verify(printStream).printf("Unable to process expression(s) '%s'%n", input); + } + + @Test + void shouldNotProcessExpressionsDuringDryRun() { + String input = "#{expr(ess)}"; + ExpressionProcessor processor = mock(); + ExpressionResolver expressionResolver = new ExpressionResolver(Collections.singleton(processor), + new PrintStreamExpressionResolverMonitor()); + assertEquals(input, expressionResolver.resolveExpressions(true, input)); + verifyNoInteractions(processor); + } + + @Test + void shouldReturnNotAStringValueForATopLevelExpression() { + String expression = "#{object(#{string()})}"; + lenient().when(targetProcessor.execute("string()")).thenReturn(Optional.of("result")); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + Object value = new Object(); + lenient().when(targetProcessor.execute("object(result)")).thenReturn(Optional.of(value)); + assertSame(value, expressionResolver.resolveExpressions(false, expression)); + } + + @ParameterizedTest + @CsvSource({ + "'#{string(#{integer()})}', '#{string(42)}'", + "'24 + #{integer()}', '24 + 42'", + "'#{integer()} + 24', '42 + 24'" + }) + void shouldConvertNotAStringValueToAStringForNotTopLevelExpression(String expression, String expected) { + lenient().when(targetProcessor.execute("integer()")).thenReturn(Optional.of(42)); + ExpressionResolver expressionResolver = + new ExpressionResolver(new LinkedHashSet<>(Arrays.asList(targetProcessor, anotherProcessor)), + new PrintStreamExpressionResolverMonitor()); + assertEquals(expected, expressionResolver.resolveExpressions(false, expression)); + } +} diff --git a/jbehave-core/src/test/java/org/jbehave/core/expressions/BiArgExpressionProcessorBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/expressions/BiArgExpressionProcessorBehaviour.java new file mode 100644 index 000000000..cee1f1dc3 --- /dev/null +++ b/jbehave-core/src/test/java/org/jbehave/core/expressions/BiArgExpressionProcessorBehaviour.java @@ -0,0 +1,55 @@ +package org.jbehave.core.expressions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Optional; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class BiArgExpressionProcessorBehaviour { + + private final BiArgExpressionProcessor processor = new BiArgExpressionProcessor<>("expr", (x, y) -> x + y); + + @ParameterizedTest + @ValueSource(strings = { + "expr(x, y)", + "expr(x,y)", + "expr( x , y )", + "expr(x\n, y\n)", + "expr(x\r\n, y\r\n)", + "expr(\nx\n,\ny\n)" + }) + void shouldMatchExpression(String expression) { + Optional actual = processor.execute(expression); + assertEquals(Optional.of("xy"), actual); + } + + @ParameterizedTest + @CsvSource({ + "expr(x,y'", + "expr( x,y", + "expr", + "exp", + "expr(x, y)", + "''" + }) + void shouldNotMatchExpression(String expression) { + Optional actual = processor.execute(expression); + assertEquals(Optional.empty(), actual); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "expr(x,y,z) | The expected number of arguments for 'expr' expression is 2, but found 3 arguments: 'x,y,z'", + "expr(x) | The expected number of arguments for 'expr' expression is 2, but found 1 argument: 'x'", + "expr() | The expected number of arguments for 'expr' expression is 2, but found 0 arguments" + }) + void shouldFailOnInvalidNumberOfParameters(String expression, String error) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> processor.execute(expression)); + assertEquals(error, exception.getMessage()); + } +} diff --git a/jbehave-core/src/test/java/org/jbehave/core/expressions/ExpressionArgumentsBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/expressions/ExpressionArgumentsBehaviour.java new file mode 100644 index 000000000..000413657 --- /dev/null +++ b/jbehave-core/src/test/java/org/jbehave/core/expressions/ExpressionArgumentsBehaviour.java @@ -0,0 +1,83 @@ +package org.jbehave.core.expressions; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.junit.jupiter.MockitoExtension; + +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +@ExtendWith(MockitoExtension.class) +class ExpressionArgumentsBehaviour { + + static Stream unlimitedExpressionArguments() { + // CHECKSTYLE:OFF + // @formatter:off + return Stream.of( + arguments("a,b", asList("a", "b")), + arguments("a, b", asList("a", "b")), + arguments(" a,b ", asList("a", "b")), + arguments(" a, b ", asList("a", "b")), + arguments("a,b,c", asList("a", "b", "c")), + arguments("a, b, c", asList("a", "b", "c")), + arguments("\"\"\"a\"\"\", b", asList("a", "b")), + arguments("\"\"\"a\"\"\", \"\"\"b\"\"\"", asList("a", "b")), + arguments("\"\"\"a,b\"\"\", \"\"\"c\"\"\"", asList("a,b", "c")), + arguments("\"\"\"a\\,b\"\"\", \"\"\"c\"\"\"", asList("a\\,b", "c")), + arguments("\"\"\" a, b \"\"\", \"\"\" c \"\"\"", asList(" a, b ", " c ")), + arguments("a\\,b,c", asList("a,b", "c")), + arguments("a,b,c\\", asList("a", "b", "c\\")), + arguments("\\a,b,c", asList("\\a", "b", "c")), + arguments(",b,c", asList("", "b", "c")), + arguments("a,b,", asList("a", "b", "")) + ); + // @formatter:on + // CHECKSTYLE:ON + } + + @ParameterizedTest + @MethodSource("unlimitedExpressionArguments") + void shouldParseUnlimitedArguments(String commaSeparatedArguments, List parsedArguments) { + ExpressionArguments argumentsMatcher = new ExpressionArguments(commaSeparatedArguments); + assertEquals(parsedArguments, argumentsMatcher.getArguments()); + } + + static Stream limitedExpressionArguments() { + // CHECKSTYLE:OFF + // @formatter:off + return Stream.of( + arguments("a,b", asList("a", "b")), + arguments("a, b", asList("a", "b")), + arguments(" a,b ", asList("a", "b")), + arguments(" a, b ", asList("a", "b")), + arguments("a,b,c", asList("a", "b,c")), + arguments("a, b, c", asList("a", "b, c")), + arguments("\"\"\"a\"\"\", b", asList("a", "b")), + arguments("\"\"\"a\"\"\", \"\"\"b\"\"\"", asList("a", "b")), + arguments("\"\"\"a,b\"\"\", \"\"\"c\"\"\"", asList("a,b", "c")), + arguments("\"\"\"a\\,b\"\"\", \"\"\"c\"\"\"", asList("a\\,b", "c")), + arguments("\"\"\" a, b \"\"\", \"\"\" c \"\"\"", asList(" a, b ", " c ")), + arguments("a\\,b,c", asList("a,b", "c")), + arguments("a,b,c\\", asList("a", "b,c\\")), + arguments("\\a,b,c", asList("\\a", "b,c")), + arguments(",b,c", asList("", "b,c")), + arguments("a,b,", asList("a", "b,")) + ); + // @formatter:on + // CHECKSTYLE:ON + } + + @ParameterizedTest + @MethodSource("limitedExpressionArguments") + void shouldParseLimitedArguments(String commaSeparatedArguments, List parsedArguments) { + ExpressionArguments argumentsMatcher = new ExpressionArguments(commaSeparatedArguments, 2); + assertEquals(parsedArguments, argumentsMatcher.getArguments()); + } +} diff --git a/jbehave-core/src/test/java/org/jbehave/core/expressions/MultiArgExpressionProcessorBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/expressions/MultiArgExpressionProcessorBehaviour.java new file mode 100644 index 000000000..efffed111 --- /dev/null +++ b/jbehave-core/src/test/java/org/jbehave/core/expressions/MultiArgExpressionProcessorBehaviour.java @@ -0,0 +1,95 @@ +package org.jbehave.core.expressions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class MultiArgExpressionProcessorBehaviour { + + private static final String EXPRESSION_NAME = "expr"; + private static final Function, String> EVALUATOR = args -> String.join("+", args); + + @ParameterizedTest + @CsvSource({ + "'expr(x, y)', x+y", + "'expr(x,y)', x+y", + "'expr( x , y )', x+y", + "'expr(x\n, y\n)', x+y", + "'expr(x\r\n, y\r\n)', x+y", + "'expr(\nx\n,\ny\n)', x+y" + }) + void shouldMatchExpressionExactNumberOfArguments(String expression, String expected) { + MultiArgExpressionProcessor processor = new MultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, + EVALUATOR); + Optional actual = processor.execute(expression); + assertEquals(Optional.of(expected), actual); + } + + @ParameterizedTest + @CsvSource({ + "'expr(x, y, z)', 'x+y+z'", + "'expr(x,y,z)', 'x+y+z'", + "'expr(x, y)', x+y", + "'expr(x,y)', x+y" + }) + void shouldMatchExpressionWithDifferentNumberOfArguments(String expression, String expected) { + MultiArgExpressionProcessor processor = new MultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, 3, + EVALUATOR); + Optional actual = processor.execute(expression); + assertEquals(Optional.of(expected), actual); + } + + @ParameterizedTest + @CsvSource({ + "expr(x,y'", + "expr( x,y", + EXPRESSION_NAME, + "exp", + "expr(x, y)", + "''" + }) + void shouldNotMatchExpression(String expression) { + MultiArgExpressionProcessor processor = new MultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, + EVALUATOR); + Optional actual = processor.execute(expression); + assertEquals(Optional.empty(), actual); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "expr(x) | The expected number of arguments for 'expr' expression is 2, but found 1 argument: 'x'", + "expr() | The expected number of arguments for 'expr' expression is 2, but found 0 arguments", + "expr(x, y, z) | The expected number of arguments for 'expr' expression is 2, but found 3 arguments: 'x, y, z'", + "expr(x,y,z) | The expected number of arguments for 'expr' expression is 2, but found 3 arguments: 'x,y,z'" + }) + void shouldFailOnInvalidNumberOfParameters(String expression, String error) { + MultiArgExpressionProcessor processor = new MultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, + EVALUATOR); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> processor.execute(expression)); + assertEquals(error, exception.getMessage()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + // CHECKSTYLE:OFF + // @formatter:off + "expr() | The expected number of arguments for 'expr' expression is from 1 to 3, but found 0 arguments", + "expr(x,y,z,a) | The expected number of arguments for 'expr' expression is from 1 to 3, but found 4 arguments: 'x,y,z,a'", + // @formatter:on + // CHECKSTYLE:ON + }) + void shouldFailOnInvalidNumberOfParametersForExpressionAcceptingRange(String expression, String error) { + MultiArgExpressionProcessor processor = new MultiArgExpressionProcessor<>(EXPRESSION_NAME, 1, 3, + EVALUATOR); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> processor.execute(expression)); + assertEquals(error, exception.getMessage()); + } +} diff --git a/jbehave-core/src/test/java/org/jbehave/core/expressions/RelaxedMultiArgExpressionProcessorBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/expressions/RelaxedMultiArgExpressionProcessorBehaviour.java new file mode 100644 index 000000000..53a66299e --- /dev/null +++ b/jbehave-core/src/test/java/org/jbehave/core/expressions/RelaxedMultiArgExpressionProcessorBehaviour.java @@ -0,0 +1,92 @@ +package org.jbehave.core.expressions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class RelaxedMultiArgExpressionProcessorBehaviour { + + private static final String EXPRESSION_NAME = "expr"; + private static final Function, String> EVALUATOR = args -> String.join("+", args); + + @ParameterizedTest + @CsvSource({ + "'expr(x, y, z)', 'x+y, z'", + "'expr(x,y,z)', 'x+y,z'", + "'expr(x,y,z,a)', 'x+y,z,a'", + "'expr(x, y)', x+y", + "'expr(x,y)', x+y", + "'expr( x , y )', x+y", + "'expr(x\n, y\n)', x+y", + "'expr(x\r\n, y\r\n)', x+y", + "'expr(\nx\n,\ny\n)', x+y" + }) + void shouldMatchExpressionExactNumberOfArguments(String expression, String expected) { + RelaxedMultiArgExpressionProcessor processor = + new RelaxedMultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, EVALUATOR); + Optional actual = processor.execute(expression); + assertEquals(Optional.of(expected), actual); + } + + @ParameterizedTest + @CsvSource({ + "'expr(x, y, z)', 'x+y+z'", + "'expr(x,y,z)', 'x+y+z'", + "'expr(x,y,z,a)', 'x+y+z,a'", + "'expr(x, y)', x+y", + "'expr(x,y)', x+y" + }) + void shouldMatchExpressionWithDifferentNumberOfArguments(String expression, String expected) { + RelaxedMultiArgExpressionProcessor processor = + new RelaxedMultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, 3, EVALUATOR); + Optional actual = processor.execute(expression); + assertEquals(Optional.of(expected), actual); + } + + @ParameterizedTest + @CsvSource({ + "expr(x,y'", + "expr( x,y", EXPRESSION_NAME, + "exp", + "expr(x, y)", + "''" + }) + void shouldNotMatchExpression(String expression) { + RelaxedMultiArgExpressionProcessor processor = + new RelaxedMultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, EVALUATOR); + Optional actual = processor.execute(expression); + assertEquals(Optional.empty(), actual); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "expr(x) | The expected number of arguments for 'expr' expression is 2, but found 1 argument: 'x'", + "expr() | The expected number of arguments for 'expr' expression is 2, but found 0 arguments" + }) + void shouldFailOnInvalidNumberOfParameters(String expression, String error) { + RelaxedMultiArgExpressionProcessor processor = + new RelaxedMultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, EVALUATOR); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> processor.execute(expression)); + assertEquals(error, exception.getMessage()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "expr(x) | The expected number of arguments for 'expr' expression is from 2 to 3, but found 1 argument: 'x'", + "expr() | The expected number of arguments for 'expr' expression is from 2 to 3, but found 0 arguments" + }) + void shouldFailOnInvalidNumberOfParametersForExpressionAcceptingRange(String expression, String error) { + RelaxedMultiArgExpressionProcessor processor = + new RelaxedMultiArgExpressionProcessor<>(EXPRESSION_NAME, 2, 3, EVALUATOR); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> processor.execute(expression)); + assertEquals(error, exception.getMessage()); + } +} diff --git a/jbehave-core/src/test/java/org/jbehave/core/expressions/SingleArgExpressionProcessorBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/expressions/SingleArgExpressionProcessorBehaviour.java new file mode 100644 index 000000000..da924d1aa --- /dev/null +++ b/jbehave-core/src/test/java/org/jbehave/core/expressions/SingleArgExpressionProcessorBehaviour.java @@ -0,0 +1,43 @@ +package org.jbehave.core.expressions; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Optional; +import java.util.function.Function; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SingleArgExpressionProcessorBehaviour { + + private final SingleArgExpressionProcessor processor = new SingleArgExpressionProcessor<>("expression", + Function.identity()); + + @ParameterizedTest + @CsvSource({ + "'expression(x)', 'x'", + "'expression()', ''", + "'expression( x )', ' x '", + "'expression(x\n)', 'x\n'", + "'expression(x\r\n)', 'x\r\n'", + "'expression(\nx\n)', '\nx\n'" + }) + void shouldMatchExpression(String expression, String expected) { + Optional actual = processor.execute(expression); + assertEquals(Optional.of(expected), actual); + } + + @ParameterizedTest + @CsvSource({ + "expression(x'", + "expression( x", + "expression", + "expressio", + "expressio(x)", + "''" + }) + void shouldNotMatchExpression(String expression) { + Optional actual = processor.execute(expression); + assertEquals(Optional.empty(), actual); + } +} diff --git a/jbehave-core/src/test/java/org/jbehave/core/junit/JUnit4DescriptionGeneratorBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/junit/JUnit4DescriptionGeneratorBehaviour.java index c267b1090..b8a71f52b 100644 --- a/jbehave-core/src/test/java/org/jbehave/core/junit/JUnit4DescriptionGeneratorBehaviour.java +++ b/jbehave-core/src/test/java/org/jbehave/core/junit/JUnit4DescriptionGeneratorBehaviour.java @@ -434,9 +434,9 @@ private void mockListBeforeOrAfterScenarioCall(ScenarioType... scenarioTypes) { Method method = new Object() {}.getClass().getEnclosingMethod(); for (ScenarioType scenarioType : scenarioTypes) { lenient().when(allStepCandidates.getBeforeScenarioSteps(scenarioType)).thenReturn( - singletonList(new BeforeOrAfterStep(method, 0, null))); + singletonList(new BeforeOrAfterStep(method, 0, Outcome.ANY, null))); lenient().when(allStepCandidates.getAfterScenarioSteps(scenarioType)).thenReturn( - singletonList(new BeforeOrAfterStep(method, 0, null))); + singletonList(new BeforeOrAfterStep(method, 0, Outcome.ANY, null))); } } diff --git a/jbehave-core/src/test/java/org/jbehave/core/steps/BeforeOrAfterStepBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/steps/BeforeOrAfterStepBehaviour.java index 996cb3556..e0ac03281 100644 --- a/jbehave-core/src/test/java/org/jbehave/core/steps/BeforeOrAfterStepBehaviour.java +++ b/jbehave-core/src/test/java/org/jbehave/core/steps/BeforeOrAfterStepBehaviour.java @@ -16,7 +16,7 @@ void shouldPassMetaToStepCreatorWhenCreatingStepWithMeta() throws Exception { StepCreator stepCreator = mock(StepCreator.class); Method method = methodFor("methodWith"); - BeforeOrAfterStep beforeOrAfterStep = new BeforeOrAfterStep(method, 0, stepCreator); + BeforeOrAfterStep beforeOrAfterStep = new BeforeOrAfterStep(method, 0, AfterScenario.Outcome.ANY, stepCreator); Meta meta = mock(Meta.class); beforeOrAfterStep.createStepWith(meta); @@ -29,7 +29,7 @@ void shouldPassMetaToStepCreatorWhenCreatingStepUponOutcomeWithMeta() throws Exc StepCreator stepCreator = mock(StepCreator.class); Method method = methodFor("methodWith"); - BeforeOrAfterStep beforeOrAfterStep = new BeforeOrAfterStep(method, 0, stepCreator); + BeforeOrAfterStep beforeOrAfterStep = new BeforeOrAfterStep(method, 0, AfterScenario.Outcome.ANY, stepCreator); Meta meta = mock(Meta.class); beforeOrAfterStep.createStepUponOutcome(meta); diff --git a/jbehave-core/src/test/java/org/jbehave/core/steps/StepCandidateBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/steps/StepCandidateBehaviour.java index 74fa7343d..5dfe616e9 100755 --- a/jbehave-core/src/test/java/org/jbehave/core/steps/StepCandidateBehaviour.java +++ b/jbehave-core/src/test/java/org/jbehave/core/steps/StepCandidateBehaviour.java @@ -56,18 +56,16 @@ import org.jbehave.core.failures.RestartingScenarioFailure; import org.jbehave.core.failures.UUIDExceptionWrapper; import org.jbehave.core.i18n.LocalizedKeywords; -import org.jbehave.core.io.LoadFromClasspath; import org.jbehave.core.model.ExamplesTable; import org.jbehave.core.model.OutcomesTable; import org.jbehave.core.model.OutcomesTable.OutcomesFailed; -import org.jbehave.core.model.TableTransformers; import org.jbehave.core.parsers.RegexPrefixCapturingPatternParser; +import org.jbehave.core.parsers.StepPatternParser; import org.jbehave.core.reporters.StoryReporter; import org.jbehave.core.steps.AbstractStepResult.NotPerformed; import org.jbehave.core.steps.AbstractStepResult.Pending; import org.jbehave.core.steps.AbstractStepResult.Successful; import org.jbehave.core.steps.StepCreator.StepExecutionType; -import org.jbehave.core.steps.context.StepsContext; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; @@ -81,14 +79,20 @@ private StepCandidate candidateWith(String patternAsString, StepType stepType, M return candidateWith(patternAsString, stepType, method, instance, new ParameterControls()); } - private StepCandidate candidateWith(String patternAsString, StepType stepType, Method method, Object instance, + private StepCandidate candidateWith(String stepPatternAsString, StepType stepType, Method method, Object instance, ParameterControls parameterControls) { - Class stepsType = instance.getClass(); + Class type = instance.getClass(); MostUsefulConfiguration configuration = new MostUsefulConfiguration(); InjectableStepsFactory stepsFactory = new InstanceStepsFactory(configuration, instance); - return new StepCandidate(patternAsString, 0, stepType, method, stepsType, stepsFactory, new StepsContext(), - keywords, new RegexPrefixCapturingPatternParser(), configuration.parameterConverters(), - parameterControls); + StepPatternParser stepPatternParser = new RegexPrefixCapturingPatternParser(); + StepCreator stepCreator = new StepCreator(type, stepsFactory, configuration.stepsContext(), + configuration.parameterConverters(), configuration.expressionResolver(), parameterControls, + stepPatternParser.parseStep(stepType, stepPatternAsString), configuration.stepMonitor(), + configuration.dryRun()); + stepCreator.useParanamer(configuration.paranamer()); + return new StepCandidate(stepPatternAsString, 0, stepType, method, type, stepsFactory, keywords, + stepPatternParser.parseStep(stepType, stepPatternAsString), stepPatternParser.getPrefix(), + stepCreator, null, configuration.stepMonitor()); } @Test @@ -172,11 +176,21 @@ public String startingWordFor(StepType stepType) { } }; - ParameterConverters parameterConverters = new ParameterConverters(new LoadFromClasspath(), - new TableTransformers()); - StepCandidate candidate = new StepCandidate("windows on the $nth floor", 0, WHEN, method, null, null, - new StepsContext(), keywords, new RegexPrefixCapturingPatternParser(), parameterConverters, - new ParameterControls()); + String stepPatternAsString = "windows on the $nth floor"; + StepPatternParser stepPatternParser = new RegexPrefixCapturingPatternParser(); + StepType stepType = WHEN; + + MostUsefulConfiguration configuration = new MostUsefulConfiguration(); + + StepCreator stepCreator = new StepCreator(null, null, configuration.stepsContext(), + configuration.parameterConverters(), configuration.expressionResolver(), + configuration.parameterControls(), stepPatternParser.parseStep(stepType, stepPatternAsString), + configuration.stepMonitor(), configuration.dryRun()); + stepCreator.useParanamer(configuration.paranamer()); + StepCandidate candidate = new StepCandidate(stepPatternAsString, 0, stepType, method, null, null, keywords, + stepPatternParser.parseStep(stepType, stepPatternAsString), stepPatternParser.getPrefix(), + stepCreator, null, configuration.stepMonitor()); + assertThat(candidate.matches("When windows on the 1st floor"), is(false)); assertThat(candidate.ignore("!-- windows on the 1st floor"), is(false)); } diff --git a/jbehave-core/src/test/java/org/jbehave/core/steps/StepCreatorBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/steps/StepCreatorBehaviour.java index 84bd25d81..e74c5b21e 100755 --- a/jbehave-core/src/test/java/org/jbehave/core/steps/StepCreatorBehaviour.java +++ b/jbehave-core/src/test/java/org/jbehave/core/steps/StepCreatorBehaviour.java @@ -862,8 +862,8 @@ private StepCreator stepCreatorUsing(SomeSteps stepsInstance, StepMatcher stepMa Configuration configuration) { InjectableStepsFactory stepsFactory = new InstanceStepsFactory(configuration, stepsInstance); return new StepCreator(stepsInstance.getClass(), stepsFactory, stepsContext, - configuration.parameterConverters(), configuration.parameterControls(), stepMatcher, - new SilentStepMonitor()); + configuration.parameterConverters(), configuration.expressionResolver(), + configuration.parameterControls(), stepMatcher, new SilentStepMonitor(), configuration.dryRun()); } private void setupContext() { diff --git a/jbehave-core/src/test/java/org/jbehave/core/steps/StepsBehaviour.java b/jbehave-core/src/test/java/org/jbehave/core/steps/StepsBehaviour.java index f7e9d0cac..2ee660243 100755 --- a/jbehave-core/src/test/java/org/jbehave/core/steps/StepsBehaviour.java +++ b/jbehave-core/src/test/java/org/jbehave/core/steps/StepsBehaviour.java @@ -12,13 +12,13 @@ import java.lang.reflect.Method; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; -import org.hamcrest.Matchers; import org.jbehave.core.annotations.AfterScenario; import org.jbehave.core.annotations.AfterStories; import org.jbehave.core.annotations.AfterStory; @@ -201,24 +201,26 @@ void shouldProvideStepsToBePerformedBeforeAndAfterScenariosWithFailureOccuring() List afterScenario = steps.listAfterScenario().get(scenarioType); assertThat(afterScenario.size(), equalTo(3)); + afterScenario.sort(Comparator.comparing(BeforeOrAfterStep::toString)); + Meta storyAndScenarioMeta = null; // uponOutcome=ANY - afterScenario.get(0).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); + afterScenario.get(1).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); assertThat(steps.afterNormalScenario, is(true)); verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "afterNormalScenarios")); // uponOutcome=SUCCESS - afterScenario.get(1).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, null); + afterScenario.get(2).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, null); assertThat(steps.afterSuccessfulScenario, is(false)); // uponOutcome=FAILURE - afterScenario.get(2).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, null); + afterScenario.get(0).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, null); assertThat(steps.afterFailedScenario, is(true)); verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "afterFailedScenarios")); } @Test - void shouldProvideStepsToBePerformedBeforeAndAfterScenariosWithNoFailureOccuring() { + void shouldProvideStepsToBePerformedBeforeScenarios() { MultipleAliasesSteps steps = new MultipleAliasesSteps(); StoryReporter repoter = mock(StoryReporter.class); @@ -229,24 +231,32 @@ void shouldProvideStepsToBePerformedBeforeAndAfterScenariosWithNoFailureOccuring beforeScenario.get(0).createStep().perform(repoter, null); assertThat(steps.beforeNormalScenario, is(true)); verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "beforeNormalScenarios")); + } + + @Test + void shouldProvideStepsToBePerformedAfterScenariosWithNoFailureOccurring() { + MultipleAliasesSteps steps = new MultipleAliasesSteps(); + StoryReporter repoter = mock(StoryReporter.class); + ScenarioType scenarioType = ScenarioType.NORMAL; List afterScenario = steps.listAfterScenario().get(scenarioType); assertThat(afterScenario.size(), equalTo(3)); + afterScenario.sort(Comparator.comparing(BeforeOrAfterStep::toString)); + Meta storyAndScenarioMeta = null; // uponOutcome=ANY - afterScenario.get(0).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); + afterScenario.get(1).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); assertThat(steps.afterNormalScenario, is(true)); verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "afterNormalScenarios")); // uponOutcome=SUCCESS - afterScenario.get(1).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); + afterScenario.get(2).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); assertThat(steps.afterSuccessfulScenario, is(true)); verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "afterSuccessfulScenarios")); // uponOutcome=FAILURE - afterScenario.get(2).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); + afterScenario.get(0).createStepUponOutcome(storyAndScenarioMeta).perform(repoter, null); assertThat(steps.afterFailedScenario, is(false)); - } @Test @@ -264,19 +274,21 @@ void shouldProvideStepsToBeNotPerformedAfterScenarioUponOutcome() { List afterScenario = steps.listAfterScenario().get(scenarioType); assertThat(afterScenario.size(), equalTo(3)); + afterScenario.sort(Comparator.comparing(BeforeOrAfterStep::toString)); + Meta storyAndScenarioMeta = null; UUIDExceptionWrapper failure = new UUIDExceptionWrapper(); // uponOutcome=ANY - afterScenario.get(0).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, failure); + afterScenario.get(1).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, failure); assertThat(steps.afterNormalScenario, is(true)); verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "afterNormalScenarios")); // uponOutcome=SUCCESS - afterScenario.get(1).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, failure); + afterScenario.get(2).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, failure); assertThat(steps.afterSuccessfulScenario, is(false)); // uponOutcome=FAILURE - afterScenario.get(2).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, failure); + afterScenario.get(0).createStepUponOutcome(storyAndScenarioMeta).doNotPerform(repoter, failure); assertThat(steps.afterFailedScenario, is(true)); verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "afterFailedScenarios")); } @@ -323,16 +335,6 @@ void shouldProvideStepsToBePerformedBeforeAndAfterAnyScenario() { verify(repoter).beforeStep(step(StepExecutionType.EXECUTABLE, "afterAnyScenarios")); } - @Test - void shouldAllowBeforeOrAfterStepsToUseSpecifiedStepMonitor() { - MultipleAliasesSteps steps = new MultipleAliasesSteps(); - List beforeStory = steps.listBeforeStory(false); - BeforeOrAfterStep step = beforeStory.get(0); - StepMonitor stepMonitor = new PrintStreamStepMonitor(); - step.useStepMonitor(stepMonitor); - assertThat(step.toString(), Matchers.containsString(stepMonitor.getClass().getName())); - } - @Test void shouldAllowLocalizationOfSteps() { Configuration configuration = new MostUsefulConfiguration();