diff --git a/examples/java-datatable/src/main/java/cucumber/examples/datatable/AnimalValidator.java b/examples/java-datatable/src/main/java/cucumber/examples/datatable/AnimalValidator.java new file mode 100644 index 00000000..acb2fb9a --- /dev/null +++ b/examples/java-datatable/src/main/java/cucumber/examples/datatable/AnimalValidator.java @@ -0,0 +1,78 @@ +package cucumber.examples.datatable; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.cucumber.core.gherkin.DataTableArgument; +import io.cucumber.plugin.ConcurrentEventListener; +import io.cucumber.plugin.Plugin; +import io.cucumber.plugin.event.EventPublisher; +import io.cucumber.plugin.event.PickleStepTestStep; +import io.cucumber.plugin.event.Step; +import io.cucumber.plugin.event.StepArgument; +import io.cucumber.plugin.event.TestStep; +import io.cucumber.plugin.event.TestStepFinished; + +/** + * This validator is enabled in the feature by using + * #validation-plugin: cucumber.examples.datatable.AnimalValidator + */ +public class AnimalValidator implements Plugin, ConcurrentEventListener { + + private ConcurrentHashMap errors = new ConcurrentHashMap<>(); + + @Override + public void setEventPublisher(EventPublisher publisher) { + publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished); + } + + private void handleTestStepFinished(TestStepFinished event) { + TestStep testStep = event.getTestStep(); + if (testStep instanceof PickleStepTestStep) { + PickleStepTestStep pickleStepTestStep = (PickleStepTestStep) testStep; + Step step = pickleStepTestStep.getStep(); + if ("the animal {string}".equals(pickleStepTestStep.getPattern())) { + StepArgument argument = step.getArgument(); + Animals animal = loadAnimal(pickleStepTestStep.getDefinitionArgument().get(0).getValue()); + if (animal == null) { + // Invalid animal! + return; + } + if (argument instanceof DataTableArgument dataTable) { + List availableData = animal.getAvailableData(); + List> cells = dataTable.cells(); + for (int i = 1; i < cells.size(); i++) { + int line = dataTable.getLine() + i; + List list = cells.get(i); + String vv = list.get(0); + if (!animal.getAvailableDataForAnimals().contains(vv)) { + errors.put(line, vv + " is not valid for any animal"); + } else if (!availableData.contains(vv)) { + errors.put(line, vv + " is not valid for animal " + animal.getClass().getSimpleName()); + } + } + } + } + } + } + //This is a magic method called by cucumber-eclipse to fetch the final errors and display them in the document + public Map getValidationErrors() { + return errors; + } + + private Animals loadAnimal(String value) { + try { + Class clz = getClass().getClassLoader() + .loadClass("cucumber.examples.datatable." + value.replace("\"", "")); + Object instance = clz.getConstructor().newInstance(); + if (instance instanceof Animals anml) { + return anml; + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + +} diff --git a/examples/java-datatable/src/test/resources/cucumber/examples/datatable.feature b/examples/java-datatable/src/test/resources/cucumber/examples/datatable.feature index f1205aff..8b93a104 100644 --- a/examples/java-datatable/src/test/resources/cucumber/examples/datatable.feature +++ b/examples/java-datatable/src/test/resources/cucumber/examples/datatable.feature @@ -1,4 +1,6 @@ #language: en +#This comment below enables the validation plugin +#validation-plugin: cucumber.examples.datatable.AnimalValidator Feature: Connection between DataTable Key and a specific Step Value diff --git a/io.cucumber.eclipse.editor/plugin.xml b/io.cucumber.eclipse.editor/plugin.xml index 5598bb71..cb437a59 100644 --- a/io.cucumber.eclipse.editor/plugin.xml +++ b/io.cucumber.eclipse.editor/plugin.xml @@ -110,6 +110,14 @@ + + + + + errors, boolean persistent) { + if (errors == null || errors.isEmpty()) { + return; + } + + mark(resource, new IMarkerBuilder() { + @Override + public void build() throws CoreException { + IMarker[] markers = resource.findMarkers(STEPDEF_VALIDATION_ERROR, true, IResource.DEPTH_INFINITE); + for (IMarker marker : markers) { + marker.delete(); + } + for (Entry entry : errors.entrySet()) { + IMarker marker = resource.createMarker(STEPDEF_VALIDATION_ERROR); + marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); + marker.setAttribute(IMarker.MESSAGE, entry.getValue()); + marker.setAttribute(IMarker.LINE_NUMBER, entry.getKey()); + marker.setAttribute(IMarker.TRANSIENT, persistent); + } + } + }); + + } + public void syntaxErrorOnStepDefinition(IResource stepDefinitionResource, Exception e) { syntaxErrorOnStepDefinition(stepDefinitionResource, e, 0); } diff --git a/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/runtime/CucumberRuntime.java b/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/runtime/CucumberRuntime.java index 56497eae..a83c4fae 100644 --- a/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/runtime/CucumberRuntime.java +++ b/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/runtime/CucumberRuntime.java @@ -150,6 +150,23 @@ public void addPlugin(Plugin plugin) { plugins.add(plugin); } + public Plugin addPluginFromClasspath(String clazz) { + try { + Class c = classLoader.loadClass(clazz); + Object instance = c.getConstructor().newInstance(); + if (instance instanceof Plugin) { + Plugin plugin = (Plugin) instance; + addPlugin(plugin); + return plugin; + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; +// plugins.add(plugin); + } + public void addFeature(GherkinEditorDocument document) { IResource resource = document.getResource(); URI uri = Objects.requireNonNullElseGet(resource.getLocationURI(), () -> resource.getRawLocationURI()); diff --git a/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/validation/CucumberGlueValidator.java b/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/validation/CucumberGlueValidator.java index 1f64a7ef..cbf3d15d 100644 --- a/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/validation/CucumberGlueValidator.java +++ b/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/validation/CucumberGlueValidator.java @@ -2,8 +2,12 @@ import static io.cucumber.eclipse.editor.Tracing.PERFORMANCE_STEPS; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -23,9 +27,11 @@ import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.jface.text.IRegion; import org.eclipse.osgi.service.debug.DebugTrace; import io.cucumber.core.gherkin.FeatureParserException; @@ -40,6 +46,7 @@ import io.cucumber.eclipse.java.plugins.CucumberStepParserPlugin; import io.cucumber.eclipse.java.plugins.MatchedStep; import io.cucumber.eclipse.java.runtime.CucumberRuntime; +import io.cucumber.plugin.Plugin; /** * Performs a dry-run on the document to verify step definition matching @@ -252,9 +259,15 @@ protected IStatus run(IProgressMonitor monitor) { rt.addPlugin(stepParserPlugin); rt.addPlugin(matchedStepsPlugin); rt.addPlugin(missingStepsPlugin); + Collection validationPlugins = addValidationPlugins(editorDocument, rt); try { rt.run(monitor); + Map validationErrors = new HashMap<>(); + for (Plugin plugin : validationPlugins) { + addErrors(plugin, validationErrors); + } Map> snippets = missingStepsPlugin.getSnippets(); + MarkerFactory.validationErrorOnStepDefinition(resource, validationErrors, persistent); MarkerFactory.missingSteps(resource, snippets, Activator.PLUGIN_ID, persistent); Collection steps = stepParserPlugin.getStepList(); matchedSteps = Collections.unmodifiableCollection(matchedStepsPlugin.getMatchedSteps()); @@ -295,6 +308,46 @@ protected IStatus run(IProgressMonitor monitor) { return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS; } + private Collection addValidationPlugins(GherkinEditorDocument editorDocument, CucumberRuntime rt) { + List validationPlugins = new ArrayList<>(); + IDocument doc = editorDocument.getDocument(); + int lines = doc.getNumberOfLines(); + for (int i = 0; i < lines; i++) { + try { + IRegion firstLine = document.getLineInformation(i); + String line = document.get(firstLine.getOffset(), firstLine.getLength()).trim(); + if (line.startsWith("#")) { + String[] split = line.split("validation-plugin:", 2); + if (split.length == 2) { + String validationPlugin = split[1].trim(); + Plugin classpathPlugin = rt.addPluginFromClasspath(validationPlugin); + if (classpathPlugin != null) { + validationPlugins.add(classpathPlugin); + } + } + } + } catch (BadLocationException e) { + } + } + return validationPlugins; + } + + } + + @SuppressWarnings("unchecked") + private static void addErrors(Plugin plugin, Map validationErrors) { + try { + Method method = plugin.getClass().getMethod("getValidationErrors"); + Object invoke = method.invoke(plugin); + if (invoke instanceof Map) { + @SuppressWarnings("rawtypes") + Map map = (Map) invoke; + validationErrors.putAll(map); + } + } catch (Exception e) { + e.printStackTrace(); + } + } }