Skip to content

Commit

Permalink
Add support for validation plugins from the project
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Christoph Läubrich authored and laeubi committed Oct 25, 2023
1 parent 6db4c4f commit 3477282
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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
* <code>#validation-plugin: cucumber.examples.datatable.AnimalValidator</code>
*/
public class AnimalValidator implements Plugin, ConcurrentEventListener {

private ConcurrentHashMap<Integer, String> 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<String> availableData = animal.getAvailableData();
List<List<String>> cells = dataTable.cells();
for (int i = 1; i < cells.size(); i++) {
int line = dataTable.getLine() + i;
List<String> 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<Integer, String> 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;
}

}
Original file line number Diff line number Diff line change
@@ -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


Expand Down
8 changes: 8 additions & 0 deletions io.cucumber.eclipse.editor/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@
<attribute name="cucumber.eclipse.marker.gherkin.unmatched_step.path"/>
<persistent value="true"/>
</extension>
<extension
id="cucumber.eclipse.marker.gherkin.validation_error"
name="Step Validation Error"
point="org.eclipse.core.resources.markers">
<super type="org.eclipse.core.resources.problemmarker"/>
<super type="cucumber.eclipse.marker"/>
<persistent value="true"/>
</extension>
<extension
point="org.eclipse.ui.editors.markerAnnotationSpecification">
<specification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class MarkerFactory {

public static final String CUCUMBER_MARKER = "cucumber.eclipse.marker";
public static final String STEPDEF_SYNTAX_ERROR = CUCUMBER_MARKER + ".stepdef.syntaxerror";
public static final String STEPDEF_VALIDATION_ERROR = CUCUMBER_MARKER + ".gherkin.validation_error";

public static final String GHERKIN_SYNTAX_ERROR = CUCUMBER_MARKER + ".gherkin.syntaxerror";

public static final String STEP_DEFINTION_MATCH = CUCUMBER_MARKER + ".stepdef.matches";
Expand Down Expand Up @@ -63,6 +65,31 @@ public class MarkerFactory {
private MarkerFactory() {
}

public static void validationErrorOnStepDefinition(final IResource resource,
Map<Integer, String> 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<Integer, String> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -252,9 +259,15 @@ protected IStatus run(IProgressMonitor monitor) {
rt.addPlugin(stepParserPlugin);
rt.addPlugin(matchedStepsPlugin);
rt.addPlugin(missingStepsPlugin);
Collection<Plugin> validationPlugins = addValidationPlugins(editorDocument, rt);
try {
rt.run(monitor);
Map<Integer, String> validationErrors = new HashMap<>();
for (Plugin plugin : validationPlugins) {
addErrors(plugin, validationErrors);
}
Map<Integer, Collection<String>> snippets = missingStepsPlugin.getSnippets();
MarkerFactory.validationErrorOnStepDefinition(resource, validationErrors, persistent);
MarkerFactory.missingSteps(resource, snippets, Activator.PLUGIN_ID, persistent);
Collection<CucumberStepDefinition> steps = stepParserPlugin.getStepList();
matchedSteps = Collections.unmodifiableCollection(matchedStepsPlugin.getMatchedSteps());
Expand Down Expand Up @@ -295,6 +308,46 @@ protected IStatus run(IProgressMonitor monitor) {
return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS;
}

private Collection<Plugin> addValidationPlugins(GherkinEditorDocument editorDocument, CucumberRuntime rt) {
List<Plugin> 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<Integer, String> 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();
}

}

}

0 comments on commit 3477282

Please sign in to comment.