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

Using Cucumber CLI programatically #786

Closed
jan-molak opened this issue Mar 14, 2017 · 25 comments
Closed

Using Cucumber CLI programatically #786

jan-molak opened this issue Mar 14, 2017 · 25 comments

Comments

@jan-molak
Copy link
Member

jan-molak commented Mar 14, 2017

Hey guys!

As part of my effort to make Serenity/JS play nice with Cucumber 2.0 (serenity-js/serenity-js#28) I'd like to invoke the Cucumber CLI programmatically from a Mocha test to avoid having to spawn a new process (which is over 20 times slower).

I appreciate that this use case is not fully compatible with the design goals of the CLI, but I think it shouldn't be difficult to make it work, not to mention that it would make my life much easier ;-).


The (simplified) structure of the code looks more or less like this:

import { clearSupportCodeFns } from 'cucumber';
const Cucumber = require('cucumber');

describe('When working with Cucumber 2.0', () => {
  afterEach(() => {
    clearSupportCodeFns();
    clearRequireCache();
  });

  it('should notify Serenity/JS of the scenario result', () => {
    const result = new Cucumber.Cli({ /* params */ }).run();

    // other assertions

    return expect(result).to.eventually.be.fulfilled;
  });

  it('should notify Serenity/JS of explicitly pending steps', () => {
    const result = new Cucumber.Cli({ /* params */ }).run();

    // other assertions

    return expect(result).to.eventually.be.fulfilled;    
  });

});

function clearRequireCache() {
  Object.keys(require.cache).forEach(key => delete require.cache[key])
}

As the CLI loads listeners, event handlers and step definitions, I'm invoking the clearSupportCodeFns() after each scenario to get rid of the support code loaded for that scenario. I'm also invoking the clearRequireCache() to remove the step definitions from the node require cache, so that the defineSupportCode function gets called again when the step defs file is loaded.

I think (?) that this should be enough to clean up any side effects, but I'm observing a weird behaviour where:

  • each scenario passes independently, but when running together only the first one does (indicating some leftover state)
  • the error that's thrown by each of the remaining scenarios complains about stacktrace-js not being able to figure out the full stack trace in getDefinitionLineAndUri. This is because the stack trace attached to the artificially generated error only contains frames related to stacktrace-js itself, which then get filtered out, leaving the list empty.

Because Cucumber receives an empty list, the getDefinitionLineAndUri function's optimistic assumption about the list containing at least one element fails.

This is manifested by the following error:

TypeError: Cannot read property 'getLineNumber' of undefined
      at getDefinitionLineAndUri (node_modules/cucumber/lib/support_code_library/helpers.js:98:24)
      at node_modules/cucumber/lib/support_code_library/helpers.js:113:34
      at src/notifier.ts:1:12564
      at node_modules/cucumber/lib/support_code_library/builder.js:82:12
      at Array.forEach (native)
      at Object.build (node_modules/cucumber/lib/support_code_library/builder.js:81:7)
      at Cli.getSupportCodeLibrary (node_modules/cucumber/lib/cli/index.js:142:32)
      at Cli.<anonymous> (node_modules/cucumber/lib/cli/index.js:149:39)
      at next (native)
      at undefined.tryCatcher (node_modules/bluebird/js/release/util.js:16:23)
      at PromiseSpawn._promiseFulfilled (node_modules/bluebird/js/release/generators.js:97:49)
      at Promise._settlePromise (node_modules/bluebird/js/release/promise.js:574:26)
      at Promise._settlePromise0 (node_modules/bluebird/js/release/promise.js:614:10)
      at Promise._settlePromises (node_modules/bluebird/js/release/promise.js:693:18)
      at Async._drainQueue (node_modules/bluebird/js/release/async.js:133:16)
      at Async._drainQueues (node_modules/bluebird/js/release/async.js:143:10)
      at Immediate.Async.drainQueues (node_modules/bluebird/js/release/async.js:17:14)

I think the issue could be resolved by either:

  1. making the getDefinitionLineAndUri slightly more defensive, so that it checks if the stack trace contains any frames
  2. replacing the call to stacktrace.getSync() with stacktrace.get(), which provides a more sophisticated implementation which I think should tackle the problem and also provide support for source maps. This however would have to be done at the expense of making the function call async.

I suppose the first option should be good enough (?) and easy to implement, but I'm not sure what should be the sensible defaults provided in the absence of the stack trace?

I see that it's fine for the uri to be set to unknown for example.


If my thinking makes sense and I didn't miss any official way of cleaning up the cucumber state, I'd be happy to submit a PR implementing the first of the suggested solutions.

Thoughts?

All the best,
Jan

@jbpros
Copy link
Member

jbpros commented Mar 15, 2017

Hey Jan,

I'm glad you're doing this! Serenity is great!

I think you shouldn't use the CLI at all in your tests but rather invoke the Runtime directly.

Could you try it and report back if anything goes wrong again or am I missing some context that prevents your from doing that?

@jan-molak
Copy link
Member Author

Hey @jbpros, thanks, the Runtime looks like a better alternative. It seems like I'd need to invoke the scenarioFilter myself and pass the filtered feature set on to the Runtime, but it seems doable.
I like the idea of being in control of the process output too, thanks for the hint :-)

I'll look into using the Runtime over the next couple of evenings. My gut feeling is though that this stacktrace.getSync() issue will come to bite me regardless...

Would you or @charlierudolph be able to see if my suggestion around defensive coding could work?

@charlierudolph
Copy link
Member

I'm good with more defensive programming and/or trying to use the async version of stacktrace. Thoughts on first trying out the async version of stacktrace and then add the defensive programming only if needed after that?

@jan-molak
Copy link
Member Author

I think this approach makes sense, according to the manual:

HEADS UP: This method does not resolve source maps or guess anonymous function names.

Which is what might be causing the problem...

In the meantime, I'll play around with using the Runtime directly as per @jbpros's suggestion

Thanks!

@charlierudolph
Copy link
Member

@jan-molak any update here?

@jan-molak
Copy link
Member Author

jan-molak commented Apr 26, 2017

Hey @charlierudolph, I ended up using the CLI. Serenity/JS should be ready to use with CucumberJS 2 shortly.

@charlierudolph
Copy link
Member

Okay. Can this issue be closed or is there more to be discussed?

@DavidGDD
Copy link

Hi guys! I've exactly the same problem as @jan-molak .

I was running my Node project programmatically with the Cucumber1 CLI and it always worked pretty well. Now, i'm trying to migrate to Cucumber2 and using the same code (adapting it to the new implementation) and it brokes at unit testing.

TypeError: Cannot read property 'getLineNumber' of undefined
    at getDefinitionLineAndUri (/node_modules/cucumber/lib/support_code_library/helpers.js:98:24)
    at Object.<anonymous> (/node_modules/cucumber/lib/support_code_library/helpers.js:81:34)
    at /node_modules/cucumber/lib/index.js:176:39
    at /node_modules/cucumber/lib/support_code_library/builder.js:77:12
    at Array.forEach (native)
    at Object.build (/node_modules/cucumber/lib/support_code_library/builder.js:76:7)
    at Cli.getSupportCodeLibrary (/node_modules/cucumber/lib/cli/index.js:137:32)
    at Cli.<anonymous> (/node_modules/cucumber/lib/cli/index.js:144:39)
    at next (native)
    at tryCatcher (/node_modules/bluebird/js/release/util.js:16:23)
    at PromiseSpawn._promiseFulfilled (/node_modules/bluebird/js/release/generators.js:97:49)
    at Promise._settlePromise (/node_modules/bluebird/js/release/promise.js:574:26)
    at Promise._settlePromise0 (/node_modules/bluebird/js/release/promise.js:614:10)
    at Promise._settlePromises (/node_modules/bluebird/js/release/promise.js:693:18)
    at Async._drainQueue (/node_modules/bluebird/js/release/async.js:133:16)
    at Async._drainQueues (/node_modules/bluebird/js/release/async.js:143:10)
    at Immediate.Async.drainQueues [as _onImmediate] (/node_modules/bluebird/js/release/async.js:17:14)
    at tryOnImmediate (timers.js:543:15)
    at processImmediate [as _immediateCallback] (timers.js:523:5)

I'm doing something like this:

const
	cucumberInfo = {
		argv: this.pluginArgs,
		cwd: process.cwd(),
		stdout: process.stdout
	},
	cucumberCli = new cucumber.Cli(cucumberInfo);

return cucumberCli.run()
	.then((succeeded) => {
                ...
	}).catch((error) => {
		...
	});

@jan-molak have you solved the problem running with the CLI?
if i use the Runtime as @jbpros suggested, Will the problem be avoided?

@jan-molak
Copy link
Member Author

@DavidGDD - yeah, CLI seems to be working fine for me - example here

@DavidGDD
Copy link

But @jan-molak, what have you change from your first version to your last version to solve the problem? I read your solution before writing but i don't see the difference... or is it in the unit test?

@jan-molak
Copy link
Member Author

I haven't changed anything between those two versions; My question was related to using Runtime class instead of the CLI; Runtime seemed cleaner but it had too many dependencies to get it to run so I settled on using the CLI class instead. Also, in my tests I'm starting a new cucumber process per test, so that might be what makes the difference.

@yaronassa
Copy link

Cucumber V1 API differs from V2.
You can't use CLI as a function anymore, you need to create a new CLI object, and pass an option argument to the constructor.

V1

//CLI would just receive an array of argument
let cli = require('cucumber').Cli(runArgs); 

//CLI.run received a callback function
return new Promise(function (resolve) {
     return cli.run(function(result){
         let exitCode = (result === true) ? 0 : 1;
         return resolve(exitCode);
     });

V2

//CLI *Constructor* requires an options object with argv, cwd and stdout
let cli = new (require('cucumber').Cli)({argv: runArgs, cwd: process.cwd(), stdout: process.stdout});

//CLI.run returns a promise
return new Promise(function (resolve, reject) {
     try {
         return cli.run()
             .then(success => resolve((success === true) ? 0 : 1));
     } catch (e) {
         return reject(e);
     }

@DavidGDD
Copy link

@jan-molak one process per test it's the cleanest workaround to the issue reported by you. With the new version it's hard to run multiple Cucumber executions in the same process due to the Node require cache.

@DavidGDD
Copy link

@yaronassa i'm running cucumber as you propose without the try/catch block and the new Promise, because chaining a catch to the promises, a throw error will be managed.

@yaronassa
Copy link

yaronassa commented Jul 25, 2017

@DavidGDD Yeah, there was an issue with unhandled rejections in BeforeFeatures that needed the try/catch (#792).
It's resolved, but I never removed the wrapper. My bad for including it in the example.

@jan-molak
Copy link
Member Author

@DavidGDD - Agreed, I'd much rather run several Cucumber executions per Node process as that would greatly speed up my integration tests. To be fair though, this use case might be slightly incompatible with Cucumber's design goal - 1 execution per process.

@DavidGDD
Copy link

Reading the features testing implementation, in the World i see the test are running using the CLI programmatically as @jan-molak and me are trying.

Executing the Cucumber features tests and logging the Stacktrace content, it always has traces, so never appears the problem with the stacktrace empty:

yarn run v0.27.5
$ yarn run lint-code && yarn run lint-dependencies
Done in 11.28s.
yarn run v0.27.5
$ mocha src


  ․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․
  ․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․․>>>>>>>> Stackframes:  [ { columnNumber: 34,
    lineNumber: 86,
    fileName: '/Users/digi/myStuff/gitHub/cucumber-js/src/support_code_library/helpers.js',
    functionName: 'getDefinitionLineAndUri',
    source: '    at getDefinitionLineAndUri (/Users/digi/myStuff/gitHub/cucumber-js/src/support_code_library/helpers.js:86:34)' },
  { columnNumber: 27,
    lineNumber: 22,
    fileName: '/Users/digi/myStuff/gitHub/cucumber-js/src/support_code_library/helpers.js',
    source: '    at /Users/digi/myStuff/gitHub/cucumber-js/src/support_code_library/helpers.js:22:27' },
...

In tests, the cucumber execution is running always with the --backtrace option enabled. Doing that in my case, the stackframes returned by Stacktracejs in stacktrace.getSync() is never empty and avoids the problem.

const
	cucumberInfo = {
		argv: this.pluginArgs.concat(['--backtrace']),
		cwd: process.cwd(),
		stdout: process.stdout
	},
	cucumberCli = new cucumber.Cli(cucumberInfo);

return cucumberCli.run()
	.then((succeeded) => {
                ...
	}).catch((error) => {
		...
	});

@binarymist
Copy link

binarymist commented Mar 14, 2018

How are we clearing the cucumber state. First test run passes:

4 scenarios (4 passed)
12 steps (12 passed)

Second always fails with:

UUUUUUUUUUUU

Warnings:
4 scenarios (4 undefined)
12 steps (12 undefined)

I'm using version 4.

The reason I'm asking is that I'm using cucumber as part of a production test service that stays running for potentially multiple test runs and it seems that there is state built up on the first test run that still exists after that, even though I'm creating a new cucumber.Cli on subsequent runs.

This works, but there must be a better way:

const { spawn } = require('child_process');
    const cucCli = spawn('node', ['./node_modules/.bin/cucumber-js', 'src/features', '-r', 'src/steps']);

    cucCli.stdout.on('data', (data) => {
      console.log(`stdout: ${data}`)
    })

    cucCli.stderr.on('data', (data) => {
      console.log(`stderr: ${data}`)
    })

    cucCli.on('close', (code) => {
      console.log(`child process exited with code ${code}`)
    })

By the way, my code is all in a hapi route handler, so I'm not sure why state would be hanging around unless I start a new process on each request.

@RalphSleighC4
Copy link

RalphSleighC4 commented Sep 17, 2018

@binarymist

Just ran into this problem today.

If you try and create more than one Cucumber.Cli in the same node process it fails because cucumber relies on side effects when requiring your support library files to define steps, and subsequent requires do not execute the files so end up with no steps defined.

Its a bit of a hack but we can force cucumber to reuse the valid support library by doing something like this:

let library = false;

module.exports = async (args) => {

    const cli = new Cucumber.Cli({ argv: args, cwd: process.cwd(), stdout: process.stdout });

    if (!library) {
        const config = await cli.getConfiguration();
        library = cli.getSupportCodeLibrary(config);
    }

    cli.getSupportCodeLibrary = () => library;
    return cli
}

But it does assume you want the same library each time (e.g. for reruns).

@binarymist
Copy link

What exactly is the support code library @RalphSleighC4 ? I'm about to move to the next stage which at this stage is spinning up a container for each cucumber instance, as I need them running in parallel. Is what you're suggesting able to be run in parallel? Also how do you use what you're suggesting, is your code snippet a module that can be consumed and run many times via run sequentually and in parallel?

@RalphSleighC4
Copy link

RalphSleighC4 commented Sep 18, 2018

It's the code for the step definitions and hooks cucumber uses to run your scenarios in the files you specify using the --require argument. If we write them in the typical style:

const { Given, When, Then } = require('cucumber')

Given('I sleep for 10 seconds', function () {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 10*1000);
  })
});

Then the first time cucumber requires them, the Given function is executed, but subsequent calls to require will return a cached version of the files module.exports, so the steps are not recreated and the run fails as undefined. Hence the need to cache the support library from the first run if you want to use the ClI more than once.

This only applies to sequential runs inside the same process. If you are using containers to run tests in parallel you won't have this issue.

@binarymist
Copy link

Thanks @RalphSleighC4, I'm comming back to this soon, I'll have a play when I get there and provide my feedback.

@binarymist
Copy link

This doesn't work for my case. I use the same step definitions and hooks, but with different data applied.

binarymist added a commit to purpleteam-labs/purpleteam-app-scanner that referenced this issue Sep 26, 2018
@binarymist
Copy link

binarymist commented Sep 26, 2018

For anyone arriving here in the future looking for help:
I now have cucumber cli working in parallel. This works with different data applied to my steps, etc.

runCuc.js wraps and modifies slightly the cli run.js. I need to also modify stdout for my application, as I'm logging locally to my parent process and also to a redis channel which ends up pushing messages via server sent events (SSE AKA eventsource) to my cli driving the SaaS.

I created a bin file similar to the cucumber-js bin file.

The whole commit is here.

All of the above is run in parallel with my app.parallel.js

For testing (as working out what's going wrong with your steps can be tricky in parallel sometimes), I conditionally run in parallel or sequence (sequence only runce one test session for me as noted in my previous comment) by checking a config value runType

Sequence looks like this

Hope this helps someone.

@lock
Copy link

lock bot commented Sep 26, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Sep 26, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants