Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cucumber JVM should provide reuseable API for Options/Parsing/Compiling/Running #1711

Open
laeubi opened this issue Jul 29, 2019 · 36 comments
Open
Labels
🏦 debt Tech debt ⚡ enhancement Request for new functionality

Comments

@laeubi
Copy link

laeubi commented Jul 29, 2019

This is related to cucumber/cucumber-eclipse#368

The JavaBackend currently has no way to access SnippetGenerator and TypeRegistry that makes it impossible for IDEs or other code to reuse the information collected by the code.

@mpkorstanje
Copy link
Contributor

What kind of information are you looking for? Directly accessing the Backend might not be the best way to acquire it.

@laeubi
Copy link
Author

laeubi commented Jul 29, 2019

In general I'd like to reuse the parsing glue as well as generation of code snippets.
Currently we parse that data by our self for the cucumber-eclipse plugin, the problem with that approach is, that this might produce inconsistencies. So it seems natural to reuse the JavaBackend for this as all the logic about finding glue-code is already there and we don't want to catch up with changes done here.

To summarize: Using the eclipse plugin should produce the same errors/output as running against cucumber-cli.

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Jul 29, 2019

In general I'd like to reuse the parsing glue as well as generation of code snippets.

In general that is going to be a problem because not all glue can be parsed without executing the code. Lamda step definitions for instance require the class that defines them is instantiated. This can be a problem when building the test context is a slow process.

But lets disregard Lambda step definitions for now.

it seems natural to reuse the JavaBackend for this as all the logic about finding glue-code is already there and we don't want to catch up with changes done here.

Specifically which bits of information are you looking for? And in what format would you like to receive them? I'd rather not open up all objects and let you look inside them -- it would make Cucumber-JVM very hard to evolve -- but we could come up with a JSON format that makes it easier.

@laeubi
Copy link
Author

laeubi commented Jul 29, 2019

I'm sure your using some kind of IDE for development (maybe Idea or Eclipse), so simply think about what would you expect from an IDE to help you write Java code. Of course it is nice to have syntax highlight, but even more helpful are features that provide you with quick-fix and suggestions for autocomplete and you want to get error/warning hints if something is probably wrong.

Especially you want to support the "Specification Guys" so the don't need to know what steps are there but can find a list on there hand, automatically generated from projects code.

Lets assume I have a feature file and want to add rich edit capabilities for the user.
So the user want's to know:

  • If I run this feature with cucumber: will it produce any error (e.g. missing step defs, wrong parameter types)
  • What steps are available and what parameter does it require
  • Get autocompletion proposals when typing, jumping from one parameter to the next
  • If a step is missing, generate me the snippet and insert it into a GlueCode file so it can be implemented

All the information and features are already available in cucumber-jvm, its just hard to access them because cucumber-jvm is specially designed for the "run once" case.
I'm not sure if it really is useful to have this as JSON since that will require another parsing step as well, I also won't expect that the API will stay stable all the time, it just must have these stuff accessible by anyone who wants to write tools for cucumber. If api change in higher versions, tools must adapt to this.

The Lamdastuff is a good example: cucumber-jvm must have some kind of logic to execute and match steps, as a tool author I don't mind HOW cucumber-jvm gather the information (even if its slow) I just don't want to implement the same logic again.

So probably I would create an instance of JavaBackend (or any other Interface!) initlize it with some options (e.g. glue packages, classloader) and then I call a method that gives me all steps that would be available the same way as if I would have run cucumber on the cli.
Next I might call a method that matches a gherkin-ast and get back any error that cucumber would report when I run it with the cli.

So to summarize: I don't wan't new exclusive features for IDE/Tools, just reuse the stuff the cucumber-cli runner will internally already do in a way that does not involve executing the feature with cli and parse the std-out of the process. To archive this I won't mind calling internal API even if that means I has to take care about changes to this internal stuff when I upgrade to a higher version.

@mpkorstanje
Copy link
Contributor

I'm not interested in considering the possibility of opening up Cucumber JVM outside of well defined interfaces and API's.

However for first three use cases you've described I believe the Plugin system would be most appropriate. You can use the StepDefinedEvent to access information about the available steps. It is currently a limited set of information but I am not opposed to extending the information that is available. The Plugin system also provides the SnippetsSuggestedEvent which should cover your fourth use case.

@laeubi
Copy link
Author

laeubi commented Jul 30, 2019

Okay so you suggest to use something like this:

Runtime runtime = cucumber.runtime.Runtime.builder().withAdditionalPlugins(new StepDefinitionReporter() {

				@Override
				public void stepDefinition(StepDefinition stepDefinition) {
					// do something with it
					
				}
			}).build();

This would be perfectly fine, i just would need more fain grained control over the "ecexute" stage, since I don't really want to execute (yet) the code, i would need something like:

  • runtime.parse() (parse all step defs)
  • runtime.compile(feature) (compile all steps against a CucumberFeature but don't run them)

So do you think the Runtime can be extended like this so the current run() is split into these "Phases" instead of one big "run()"?

@mpkorstanje
Copy link
Contributor

If you're looking to do that then you might be better of assembling the run time components yourself like the JUnit and TestNG Runner do.

@laeubi
Copy link
Author

laeubi commented Jul 30, 2019

That might be possible but I would prefer not to replicate all the mechnism. If JUnit an TestNG already do this, wouldn't it be valid to provide reuseable API that

  • CLI
  • Junit
  • TestNG
  • Tools/IDEs can use...

@laeubi laeubi changed the title JavaBackend should provide getter for SnippetGenerator and TypeRegistry Cucumber JVM should provide reuseable API for Options/Parsing/Compiling/Running Jul 30, 2019
@mpkorstanje
Copy link
Contributor

We used to have this design in v1 and v2. The Runtime was used directly in by the CLI, JUnit and TestNG. It was unsatisfactory.

The CLI, JUnit TestNG each impose a different structure on the way tests are discovered, organized, filtered and executed. Because each of the runners is interested in low level concepts - discovery, features, scenarios, filtering- this structure does not generalize into a single Runtime structure.

You might have some luck extracting a generic runner factory of sorts but looking forward to Junit5 I think even that will be done differently by then.

@mpkorstanje
Copy link
Contributor

I see you've just changed the title of the issue. That would be a reasonable request but I don't think the current internal implementation is stable yet.

@mpkorstanje
Copy link
Contributor

If you're looking to contribute something you could start with the develop-v5 branch. The options parsing there is quite stable, and quite repetitive. Should be ideal to construct a builder out of.

@laeubi
Copy link
Author

laeubi commented Aug 8, 2019

I have started to investigate using StepDefinition from event, the problem is that I can't get access to the actual method, do you think it would be ok to add a getter for the method that belongs to the step definition?

@mpkorstanje
Copy link
Contributor

What do you need the method object for? You don't need the execute method. Would the canonical name/signature and location be sufficient?

@timtebeek
Copy link
Contributor

Came across this issue and made me toy around with writing a plugin / runner in hopes it helps:

import io.cucumber.core.event.*;
import java.util.Map;
import java.util.TreeMap;

public class StepDiscoveryPlugin implements EventListener{

	private final Map<String, String> definedSteps = new TreeMap<>();
	private final Map<String, Status> finishedSteps = new TreeMap<>();
	private final Map<String, Status> finishedCases = new TreeMap<>();

	@Override
	public void setEventPublisher(EventPublisher publisher) {
		publisher.registerHandlerFor(StepDefinedEvent.class, this::handleStepDefinedEvent);
		publisher.registerHandlerFor(TestCaseFinished.class, this::handleTestCaseFinished);
		publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished);
	}

	private void handleStepDefinedEvent(StepDefinedEvent event) {
		definedSteps.put(event.getStepDefinition().getLocation(false), event.getStepDefinition().getPattern());
	}

	private void handleTestStepFinished(TestStepFinished event) {
		finishedSteps.put(event.getTestStep().getCodeLocation(), event.getResult().getStatus());
	}

	private void handleTestCaseFinished(TestCaseFinished event) {
		finishedCases.put(event.getTestCase().getUri() + ':' + event.getTestCase().getLine(),
				event.getResult().getStatus());
	}

	public Map<String, String> getDefinedSteps() {
		return definedSteps;
	}

	public Map<String, Status> getFinishedSteps() {
		return finishedSteps;
	}

	public Map<String, Status> getFinishedCases() {
		return finishedCases;
	}
}

Which can then be run through something like:

RuntimeOptions runtimeOptions = new RuntimeOptionsBuilder()
		.setDryRun()
		.build();
StepDiscoveryPlugin stepDiscoveryPlugin = new StepDiscoveryPlugin();
Runtime runtime = Runtime.builder()
		.withRuntimeOptions(runtimeOptions)
		.withAdditionalPlugins(stepDiscoveryPlugin)
		.build();
runtime.run();

Map<String, String> definedSteps = stepDiscoveryPlugin.getDefinedSteps();
Map<String, Status> finishedSteps = stepDiscoveryPlugin.getFinishedSteps();
Map<String, Status> finishedCases = stepDiscoveryPlugin.getFinishedCases();
System.out.println("definedSteps=" + definedSteps);
System.out.println("finishedSteps=" + finishedSteps);
System.out.println("finishedCases=" + finishedCases);

Which will print something like:

definedSteps={
	CucumberSteps.java:37=een ophaalverzoek,
	CucumberSteps.java:41=een verlopen ophaalverzoek,
	CucumberSteps.java:45=er is meetdata beschikbaar,
	CucumberSteps.java:46=er is te nieuwe meetdata beschikbaar,
	CucumberSteps.java:47=er is geen meetdata beschikbaar,
	CucumberSteps.java:49=het ophaalverzoek binnenkomt,
	CucumberSteps.java:52=het ophaalverzoek veelvuldig binnenkomt,
	CucumberSteps.java:59=wordt er meetdata teruggestuurd,
	CucumberSteps.java:65=wordt er geen meetdata teruggestuurd,
	CucumberSteps.java:70=wordt er een leeg bericht teruggestuurd,
	CucumberSteps.java:77=hetzelfde ophaalverzoek nogmaals binnenkomt,
	CucumberSteps.java:80=wordt hetzelfde bericht teruggestuurd},
	
finishedSteps={
	CucumberSteps.java:33=SKIPPED,
	CucumberSteps.java:34=SKIPPED,
	CucumberSteps.java:35=SKIPPED,
	CucumberSteps.java:37=SKIPPED,
	CucumberSteps.java:41=SKIPPED,
	CucumberSteps.java:45=SKIPPED,
	CucumberSteps.java:46=SKIPPED,
	CucumberSteps.java:47=SKIPPED,
	CucumberSteps.java:52=SKIPPED,
	CucumberSteps.java:59=SKIPPED,
	CucumberSteps.java:65=SKIPPED,
	CucumberSteps.java:70=SKIPPED,
	CucumberSteps.java:77=SKIPPED,
	CucumberSteps.java:80=SKIPPED},
	
finishedCases={
	classpath:cucumber/somefeature.feature:10=SKIPPED,
	classpath:cucumber/somefeature.feature:16=SKIPPED,
	classpath:cucumber/somefeature.feature:22=SKIPPED,
	classpath:cucumber/somefeature.feature:4=SKIPPED,
	classpath:cucumber/anotherfeature.feature:13=SKIPPED,
	classpath:cucumber/anotherfeature.feature:4=SKIPPED}]

@timtebeek
Copy link
Contributor

@laeubi could the above help in the eclipse plugin development? I'm unfamiliar with IDE plugin development constraints, but it would seem to me you have all the parts to make this work, since the Eclipse plugin is already able to run features. Please note that it's enough to run in dryMode, so this can happen in the background without side effects.

@laeubi
Copy link
Author

laeubi commented Aug 10, 2019

@mpkorstanje I need it to show in code editor or jump to the location. I just don't want to parse strings I wan't to use standard java api from objects already there...
So to be sure the matched method would be optimal.
@timtebeek yeah I'm currently working with something similar to this, the problem I encountered is that I can't get line information in my testing, maybe special compilersettings/jvm versions are required fro this?

@mpkorstanje
Copy link
Contributor

  1. To the best of my knowledge it is not possible to get a file/line number from a method object. They're only available through stack trace elements. So how would you do this?

  2. Not every backed uses methods. Nor would I like to impose this on a backend. So without exposing implementation details you will have to deal with some intermediate object. To make it even more interesting, the java backend can only provide signature information while the java 8 and groovy backends can provide file and line number.

@laeubi
Copy link
Author

laeubi commented Aug 11, 2019

  1. If I have all method information available (modifier, parameters, name, class that defines the method, annotations) I can match them with the given source AST
  2. That is true I just checked the StepDefintion class and it obviously has a method internally so you violates your own "rule" here. I also don't see how an intermediate object would solve the issue (since if the backend has no method information you are out of luck) but anyways a real Object (call it StepLocationInformation or something) would be sufficient it should (as far as I can tell for now at least the following information:
  • Fully qualified class-name (or what ever identifies the object that introduces the step)
  • Line number in source file if possible (or -1 to indicate it was not possible to gain that information)
  • Name of method (or whatever identifies the glue that defines the step)
  • Parameter types
  • Annotation information (When/Then/And...)

This would allow to match source with parsed step definition in most cases.

@laeubi
Copy link
Author

laeubi commented Aug 11, 2019

btw Groovy-Support is also on the todo-list, so if this would work on the groovy-backend as well this would be nice.

@drivera73
Copy link

drivera73 commented Apr 28, 2020

I have an interesting, if unrelated issue which might benefit from this sort of API access. I'm currently developing "user simulators" - automated processes that simulate user activity - which for now are being programmed manually.

The simulators use Selenium WebDriver (i.e. send clicks, keystrokes, etc., through a surrogate browser instance), and work fairly well as-is. We can very quickly scale up or down the number of users, etc.

However, modifying the script the simulators are on requires code changes.

Enter Gherkin (and by now you see where I'm going with this).

Using Gherkin we could then build a library of "scripts" (i.e. Gherkin features/scenarios) which we could then feed to the underlying engine (i.e. Cucumber) at our "leisure" in order to run the simulated scenarios. I'm well aware that this is only a subset of what an engine like Cucumber can do, and is a bit deviant from the design intent (automated testing).

However, this seems like a valid enough use case to wonder: why is this (currently) not possible?

I poked around the code a bit to try to figure out if there was some pattern that could be followed to run ONE scenario (or, failing that, a feature file). If that became available we could easily come up with a JMeter plugin that allows features to be defined and executed dynamically during user simulation.

As a side note, we're using JMeter since it affords us two important things: thread management and built-in metrics tracking/reporting.

So....thoughts?

If there already examples of how to do this, then I'd certainly like to see one since my searches have come up empty so far.

What I'm (ideally) looking for is something like this:

public void initGherkinEngine() {
    // initialize all the Cucumber stuff, class introspector/scanner, etc., but NOT feature script loading
    this.cucumberEngine = new CucumberEngine();
    // ... more configuration ... /
    this.cucumberEngine.configure();
}

/** This function would get called time and again for each "Gherkin Sampler" defined in JMeter.
  * Thread safety and all that would be handled by our code as well (i.e. if the cucumber engine
  * is stateful and can't be reused by different threads).  Thus, we initialize the "big" stuff as
  * few times as possible, and simply "run" the features as if they were scripts as we go...
  */
public void runFeature(String feature) {
    CompiledFeature c = FeatureCompiler.compile(feature);
    this.cucumberEngine.execute(c);
}

public void closeGherkinEngine() {
    // shut down the Cucumber stuff cleanly, if even necessary
    this.cucumberEngine.close();
}

Thanks!

@laeubi
Copy link
Author

laeubi commented Apr 29, 2020

You can use the cucumber-main as a blueprint and adjust it to your needs. Another way would be to call the Main via javacode and passing the necessary parameters but since most of the stuff is private in that class you won't have much control over the flow.
But essentially you describe what an IDE would also need, controlled configuration/parsing of the flow, e.g. if you running Junit tests in eclipse it is possible to run one specific test method, similar to this (and if I understand you right) it would be good to run one scenario, feature or even selected line of a table without the need of reconfigure/rewrite the feature itself.

@mpkorstanje
Copy link
Contributor

@drivera73 it might be worth looking at JUnit 5. In combination with junit-platform-engine you can discover/select/execute scenarios through the JUnit Platform Launcher API.

https://junit.org/junit5/docs/current/user-guide/#launcher-api

@drivera73
Copy link

@laeubi I saw the cucumber-main and tried to use it as a blueprint, but ran into the drawback that each time I would want to run a single feature/story (script), I'd have to (re-)initialize the entire framework from scratch.

What I'm looking for is some method or mechanism through which I can initialize the framework in terms of the story step implementations (Given, When, Then, etc.), and then selectively run one story at a time, in whatever order I choose.

Imagine a GUI where I can basically define the engine's base step lookup mechanics by some unspecified means, and upon clicking a "start" button I'm presented with a TextArea input where I type in the story I wish to run, along with 5 buttons: load, save, compile (i.e. validate syntax), run, and clear (clear the text area).

While that's not the tool I'm looking to implement it embodies everything I would need:

  • Initialize the core engine once and only once for all threads
  • Execute stories (scripts) piecemeal without requiring them to be stored on the filesystem anywhere (i.e. fed in as a string, for instance), in a multithreaded manner, hopefully reusing the same engine instance
  • No requirements of additional software (i.e. adding portions of JUnit 5 like @mpkorstanje suggests adds an undue burden of extra code that shouldn't be necessary)

Thoughts?

@drivera73
Copy link

drivera73 commented Apr 29, 2020

To clarify: in my use case I'd like to scan the class libraries for the Cucumber step tags only once, since doing that task is likely to be fairly time-consuming. That's what initGherkinEngine() from my above pseudo-code example would do. Basically, any and all initialization of objects that don't need to be built time-and-again when running each feature would happen once per execution thread.

Then, once the context is initialized (classes scanned, steps indexed, etc.) I would reuse that context whenever I wish to run a specific story. Then I would simply obtain the story/feature as a string (or somesuch) to be fed into runFeature() for execution. This could be done by many threads at once so the context (which is meant to be re-used) would have to be hardened for that purpose (unless we're using one per thread). Many unrelated stories may end up being run in succession by the same thread. Managing the state within the step implementations is out of scope for this discussion.

Finally, when all is done, any and all cleanup would be handled by closeGherkinEngine().

Under this model, it doesn't matter where the features/stories would be loaded from. We'd have to be careful to avoid name/string/syntax collisions between steps since the implementations would be strewn across a myriad of different classes whose (class)path would now be irrelevant, since we're not looking to do automatic matching of a story to its step implementations.

Based on what I read in the code so far, there's no way to do this currently exposed... is this impression correct? Adding code beyond Cucumber (and required dependencies) is out of the question - we're trying to be as strict as possible with the size of our binary for reasons that aren't relevant to this conversation.

So...any ideas?

Thanks!

@laeubi
Copy link
Author

laeubi commented Apr 30, 2020

@drivera73 I fully agree that this would be helpful and I'm partly tried to implement something in this direction but that's a little bit cumbersome because you have to adjust whenever cucumber extends/change... not sure if this was improved in cucumber5 line.

@drivera73
Copy link

Just as an FYI, I was able to do something close to what I want to do using JBehave. It supports Gherkin as well as its own Gherkin-like syntax (not using this for now) and even though it may not be 100% the syntax that Cucumber supports, it's good enough for my current purposes. I look forward to the day I can support this type of functionality with Cucumber.

Cheers!

@timtebeek
Copy link
Contributor

JUnit 5.7.0 has been released, with notable new addition: https://junit.org/junit5/docs/5.7.0/release-notes/

  • New FilePosition support in FileSelector and ClasspathResourceSelector.

I believe that was what was needed for this issue to continue; Am I right @laeubi ?

@timtebeek
Copy link
Contributor

So @mpkorstanje just added the new FilePosition handling in #2121 ; Would that be enough to continue with the cucumber-eclipse plugin @laeubi / @qvdk ?

@qvdk
Copy link
Member

qvdk commented Sep 22, 2020

Hi
Probably!
However the eclipse plugin need an important rewrite.
There are a custom parsing method on step definition detection (used to find them thanks sources and not binaries classes). This should be reworked to delegate the job to cucumber-jvm, and then we could use this improvement.

@laeubi
Copy link
Author

laeubi commented Jan 15, 2021

@mpkorstanje its been a while but now I'm back to the topic and whonder if in the meanwhile there is some support to supply a set of source/classfiles and let cucumber-jvm scan them for stepdefs like with gherkin described here https://github.com/cucumber/cucumber/tree/master/gherkin ?

@mpkorstanje
Copy link
Contributor

Can't do that without executing Cucumber. Step definitions classes currently have to be instantiated (e.g. cucumber-java8) to register step definitions. You may do some static analysis but that's well out side the scope of this project.

@mpkorstanje
Copy link
Contributor

That said, you may have some luck with a dry-run in combination with the output from the messages plugin. It contains some information about the step definitions.

@laeubi
Copy link
Author

laeubi commented Jan 18, 2021

@mpkorstanje thanks the dry-run seems to work so far and gives me the step definition information! The only thing I noticed is that I also need to provide a dummy feature file, otherwise cucumber simply do nothing but I assume that's intentional?!

@laeubi
Copy link
Author

laeubi commented Feb 15, 2021

I'd like to give some feedback here. I have now used the dry-run option together with a custom plugin that works really nice. I'm currently using the "Runtime" class as a backend and have written a little wrapper around it mainly to conveniently collect some data (plugins, features) before I start the run and convert some internal data structures:

https://github.com/cucumber/cucumber-eclipse/blob/cucumber6/io.cucumber.eclipse.java/src/io/cucumber/eclipse/java/runtime/CucumberRuntime.java

@mpkorstanje already mentioned that this is not really API and seems to have some drawbacks but I found the Runtime usage quite comfortable, flexible and has given good results. So maybe this class can be enhanced to become a public API.

I also used the Main/CLI class that also has worked, but I'm missing there the opportunity to pass Plugins/Features as Java objects (as sometimes a feature might only reside in memory or a plugin requires additional parameter that can't be passed as text, and sometime I wan't to access data after the run is over). It also requires me to build some strings to pass parameters and change runtime options what is a bit crazy (I need to make strings out of my object so cucumber can parse the strings into runtime options).

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Jun 11, 2021

The JUnit5 API is a nice API to copy. If you squint a bit you can already see that the Runtime.Builder has some aspects of it.

FeatureProvider featureProvider = ...;
GlueProvider glueProvider = ...;
Extension diProvider = ...;

Launcher launcher = create(builder()
        .addFeatureProvider(featureProvider)
        .addGlueProvider(glueProvider)
        .addDependencyInjectionProvider(diProvider)
        .build());

TestPlan testPlan = launcher.discover(request()
        .configurationParameter("config.key", "value")
        .filters(tagExpression("@gherkin and not @zukini"))
        .selectors(selectPackage("com.example"))
        .selectors(selectDirectory("com/example"))
        .build());

launcher.execute(testPlan, listeners);

A few important points:

  • Discovery and execution should be separated. In part to facilitate JUnit 5.
  • The Launcher is stateless and only contains stateless providers.
  • The *Providers are stateless services that can also be discovered via SPI.
  • The *Providers will be given a request to do something and the request will also contain the configuration parameters
  • Test discovery and execution are separated
  • Listeners/Plugins are entirely external to the test execution and discovery lifecycle.

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Jun 11, 2021

This API doesn't really facilitate IDEs yet:

IDE's would need to be able to:

  • Implement their own glue provider that can work with the result of static analysis
  • Given a step, discover which step definitions match it
  • Be able to correlate step definitions back to a location in source
  • Access the TreeRegex for that step definition
  • Link parameters in a step definition to their definition
  • Link arguments in a step definition to their definition, datatable type or docstring type

I think this mostly comes down to how the glue API and the Gherkin parser API are designed.

mpkorstanje pushed a commit that referenced this issue Sep 9, 2021
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏦 debt Tech debt ⚡ enhancement Request for new functionality
Projects
None yet
Development

No branches or pull requests

6 participants