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

Command parsing hangs under unit test #330

Closed
john-u opened this issue Dec 30, 2021 · 17 comments
Closed

Command parsing hangs under unit test #330

john-u opened this issue Dec 30, 2021 · 17 comments
Labels
bug Something isn't working

Comments

@john-u
Copy link

john-u commented Dec 30, 2021

We have been using oclif v1 and unit test our CLI using jest. We follow the instruction in the oclif docs to use the static Command.run inside tests. When attempting to migrate the project from v1 to core, several of the tests had timeout failures which signified a hanging handler. I dug into the parsing differences between v1 and core and found that core has a new feature which falls back to reading from stdin if there are no arguments specified. This causes issues as our CLI has several commands which have only optional arguments. I am not aware of this being a known breaking change, as it isn't mentioned on the migration guide.

Bug

I believe a summary of the issue is that calling Command.run() without args under test causes a hang at this line in the parser (waiting for input that will never arrive).

Current (core) behavior

Here is an example of one of our tests that is hanging with core.

await expect(AppRegisterCommand.run()).resolves.not.toThrow() // note the omitted run args

It fails with the following.

: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout

The test just below it however passes on the same command since it includes args.

await expect(AppRegisterCommand.run([appId])).resolves.not.toThrow() // including any arg works around the hang

✕ calls selectFromList with correct config (5005 ms)
✓ calls correct endpoint for registration and logs success (18 ms)

Environment

"@oclif/core": "^1.0.11 "
darwin-x64 node-v12.18.1

@peternhale
Copy link
Contributor

Hi @john-u, thank you for raising this issue. If possible, can you share the project you are working on so I might give it a go?

@john-u
Copy link
Author

john-u commented Dec 30, 2021

@peternhale
Copy link
Contributor

Thank you

@peternhale
Copy link
Contributor

@john-u The master branch is using oclif v1 in cli package. Is there a branch I can fetch that has the changes to use core?

@john-u
Copy link
Author

john-u commented Dec 30, 2021

Yes sorry about that, I forgot to push my branch. Here is a PR SmartThingsCommunity/smartthings-cli#238

@peternhale
Copy link
Contributor

@john-u I just ran the test you mentioned in the original post packages/cli/src/__tests__/commands/apps/register.test.ts. While it is failing, it is not hanging.

Error: expect(received).resolves.not.toThrow()

Received promise rejected instead of resolved
Rejected to value: [Error: Unexpected arguments: --reporters, /Applications/WebStorm.app/Contents/plugins/JavaScriptLanguage/helpers/jest-intellij/lib/jest-intellij-reporter.js, --verbose, --runTestsByPath, /Users/peter.hale/oclifProjects/smartthings-cli/packages/cli/src/__tests__/commands/apps/register.test.ts
See more help with --help]

Not what the state of this test might be.

Running on Mac 11.6.2
node --version
v16.13.1

@john-u
Copy link
Author

john-u commented Dec 30, 2021

@peternhale I noticed that as well while investigating this. If Command.run() has no args, oclif falls back to grabbing them from process. In this case, it doesn't hang but the args it got were meant for jest. I think the workaround for us here would be to ensure that an empty arg array is given to run Command.run([]) then oclif won't look at the process for args. I feel like this is related to the cause of the initially reported issue.

@peternhale
Copy link
Contributor

@john-u I tried the suggestions of adding the [] to run. Now the test fails with a timeout.

: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: 

I will discuss this with team mates, next week.

@peternhale peternhale added the bug Something isn't working label Dec 30, 2021
john-u added a commit to john-u/smartthings-cli that referenced this issue Dec 30, 2021
@peternhale
Copy link
Contributor

peternhale commented Dec 30, 2021

@john-u I believe I have reproduced this using a simpler project. Command in question does define, so not sure if the timeout I am seeing is the same that you are seeing.

static args = [{name: 'person', description: 'Person to say hello to', required: true}]

Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

      2 | 
      3 | describe('hello', async () => {
    > 4 |   it('runs hello cmd', async () => {
        |   ^
      5 |     await Hello.run([])
      6 |   })
      7 | })

@john-u
Copy link
Author

john-u commented Dec 31, 2021

@john-u I believe I have reproduced this using a simpler project. Command in question does define, so not sure if the timeout I am seeing is the same that you are seeing.

static args = [{name: 'person', description: 'Person to say hello to', required: true}]

Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

      2 | 
      3 | describe('hello', async () => {
    > 4 |   it('runs hello cmd', async () => {
        |   ^
      5 |     await Hello.run([])
      6 |   })
      7 | })

If only one arg is specified as required, I would expect oclif to throw Missing 1 required arg instead of hanging, so I'm also not sure that Timeout is the same case. My mention about our commands having only optional args was just in reference to our need to unit test them without specifying any, as some logic may depend on the omission of args in those cases.

john-u added a commit to john-u/smartthings-cli that referenced this issue Jan 4, 2022
john-u added a commit to SmartThingsCommunity/smartthings-cli that referenced this issue Jan 4, 2022
@tft7000
Copy link

tft7000 commented Feb 3, 2022

Maybe I experienced the same problem but with @oclif/core itself:

I have a gitlab pipeline with semantic release for an oclif app that's based on @oclif/core.
Within npm publish the oclif manifest command (referenced in package.json prepack script) hangs. Unfortunatly I can only reproduce it within the pipeline but not manually. Neither in the agent container nor anywhere else.
However I could trace the problem to this line: https://github.com/oclif/core/blob/main/src/parser/parse.ts#L243 (that got initiated by the the oclif Manifest command's const {args} = await this.parse(Manifest) line here https://github.com/oclif/oclif/blob/main/src/commands/manifest.ts#L17):

// parser/parse.ts around line 243
if (!arg.ignoreStdin && !stdinRead) {
        // eslint-disable-next-line no-await-in-loop
        let stdin = await readStdin() // <--- here the script hangs in a automated pipeline
        if (stdin) {
          stdin = stdin.trim()
          args[i] = stdin
        }

        stdinRead = true
}

so my current workaround is to provide the default value for optional arguments.

@peternhale
Copy link
Contributor

@tft7000 Thank you for raising this issue. We will have a look.

@MonochromeChameleon
Copy link
Contributor

I've been looking into this today and it looks like the problem is in the parser inside that readStdin() call that @tft7000 identified. Where on line 26 of parse.ts it has:

if (stdin.isTTY) return result

the isTTY property isn't inherited by spawned child processes, and so is undefined rather than True. Changing it to

if (stdin.isTTY || stdin.isTTY === undefined) return result;

fixes the problem for me.

Happy to raise a PR over the weekend if that would be helpful

@mdonnalley
Copy link
Contributor

@MonochromeChameleon @john-u 1.3.1 has the fix from #363 - can you let me know if you're still seeing this issue?

@tft7000
Copy link

tft7000 commented Feb 7, 2022

@mdonnalley : from my side I can say that with the fix from #363 in v1.3.1 oclif manifest works now as expected within a pipeline. thanks @MonochromeChameleon for the work.

@MonochromeChameleon
Copy link
Contributor

Everything is working for me with version 1.3.1 as well - thanks for getting it merged and released so quickly!

@john-u
Copy link
Author

john-u commented Feb 7, 2022

1.3.1 has resolved the hanging tests we were seeing as described above. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants