From 34772829b0731efb24a48284d07e35328fc78314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 24 Oct 2023 17:34:33 +0200 Subject: [PATCH] Add support for validation plugins from the project Currently it is only possible to validate the glue on execution but it some cases it would be useful to have some prevalidation. This adds support for specify validation plugins in the glue code that are executed as part of the parsing of document and show up as errors immediately on save. --- .../examples/datatable/AnimalValidator.java | 78 +++++++++++++++++++ .../cucumber/examples/datatable.feature | 2 + io.cucumber.eclipse.editor/plugin.xml | 8 ++ .../eclipse/editor/marker/MarkerFactory.java | 27 +++++++ .../eclipse/java/runtime/CucumberRuntime.java | 17 ++++ .../validation/CucumberGlueValidator.java | 53 +++++++++++++ 6 files changed, 185 insertions(+) create mode 100644 examples/java-datatable/src/main/java/cucumber/examples/datatable/AnimalValidator.java 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(); + } + } }