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

Bail option to skip all subsequent tests #1459

Closed
4 tasks done
jgoux opened this issue Jun 9, 2022 · 22 comments · Fixed by #3163
Closed
4 tasks done

Bail option to skip all subsequent tests #1459

jgoux opened this issue Jun 9, 2022 · 22 comments · Fixed by #3163
Assignees
Labels
enhancement New feature or request pr welcome

Comments

@jgoux
Copy link
Contributor

jgoux commented Jun 9, 2022

Clear and concise description of the problem

Depending on the kind of tests we run, it can be interesting to have a fail-fast feature and exit as soon as a test is failing.

I'm not the only one needing such feature apparently: #964

Jest has bail for this purpose.

Suggested solution

As Vitest tries to smooth the transition from Jest, I think the bail option is a sensible name.

But Jest implementation is incomplete/weird, it's only skipping the next test suites (describe blocks), but it continues to run the other tests within the same test suite.

I think the default should be to bail by test case and not by test suite.

# Skip all the subsequent tests after n test failures (default 1)
--bail <n>

I'm not sure what could be the API if we want to deal with both test cases and test suites, maybe adding a --bail-scope <test|suite> (default test)? 🤔

Alternative

No response

Additional context

Here are related issues about it:

Validations

@sheremet-va
Copy link
Member

Can you explain how you see it? For example, what should happen in those situations:

  1. test failed without describe
test('test 1', () => {/* code that fails */})
test('test 2', () => {/* other test */})
  1. test failed inside describe
describe('describe 1', () => {
  test('test 1', () => {/* code that fails */})
  test('test 2', () => {/* other test */})
})

describe('describe 2', () => {
  test('test 1', () => {/* other test */})
  test('test 2', () => {/* other test */})
})
  1. test failed inside nested describe
describe('describe 1', () => {
  describe('describe 2', () => {
    test('test 1', () => {/* code that fails */})
    test('test 2', () => {/* other test */})
  })

  test('test 1', () => {/* other test */})
  test('test 2', () => {/* other test */})
})
  1. test fails in one spec
// spec1.spec.ts
test('test 1', () => {/* code that fails */})
// spec2.spec.ts
test('test 2', () => {/* other test */})

@jgoux
Copy link
Contributor Author

jgoux commented Jun 10, 2022

  1. test failed without describe
test('test 1', () => {/* code that fails */})
test('test 2', () => {/* other test */})

skip test 2 in both test and suite scopes.

  1. test failed inside describe
describe('describe 1', () => {
  test('test 1', () => {/* code that fails */})
  test('test 2', () => {/* other test */})
})

describe('describe 2', () => {
  test('test 1', () => {/* other test */})
  test('test 2', () => {/* other test */})
})

skip all the tests in test scope
skip only "describe 1"."test 2" in suite scope

  1. test failed inside nested describe
describe('describe 1', () => {
  describe('describe 2', () => {
    test('test 1', () => {/* code that fails */})
    test('test 2', () => {/* other test */})
  })

  test('test 1', () => {/* other test */})
  test('test 2', () => {/* other test */})
})

skip all the tests in test scope
skip only "describe 2"."test 2" in suite scope

  1. test fails in one spec
// spec1.spec.ts
test('test 1', () => {/* code that fails */})
// spec2.spec.ts
test('test 2', () => {/* other test */})

skip all the tests in test scope
skip only other tests in spec1.spec.ts in suite scope

I think the rule should be:

  • using --bail-scope test, you see the whole tests as a flatten structure, if one test fail, no matter where it fails (in another file, in a describe, whatever) we skip everything else and display the report.
  • using --bail-scope suite, you skip the subsequent tests in the same "unit" as the failed test. Keep in mind that this "unit" can be logical, if you have 2 describe blocks named the same in two different test files, they are the same "unit". Also, it's more predictable to stop at the closest "unit", if you have nested describe, I don't think you want to skip the tests in the parent describe.

@sheremet-va
Copy link
Member

Thank you for your clarification. I have some notes tho.

if you have 2 describe blocks named the same in two different test files, they are the same "unit"

This wouldn't be possible. One spec file knows nothing about the other, they are isolated (as they should be). And it doesn't really makes sense that test in one file can affect test in another.

I also don't like the name "test" for the scope. I personally get confused associating scope test with all tests, maybe we can choose a better name?

Also, should we have some escape hatch for specifying scope suite for some files, like describe.bail? Or that would be an overkill?

@jgoux
Copy link
Contributor Author

jgoux commented Jun 12, 2022

This wouldn't be possible. One spec file knows nothing about the other, they are isolated (as they should be). And it doesn't really makes sense that test in one file can affect test in another.

I thought it was something possible with Jest, as you can nest describe blocks, it can make sense to have big groups to organize your tests (they don't really affect each other, it's more of an organization thing).

I also don't like the name "test" for the scope. I personally get confused associating scope test with all tests, maybe we can choose a better name?

Yes maybe test|suite is too vague, how about single|group?

Also, should we have some escape hatch for specifying scope suite for some files, like describe.bail? Or that would be an overkill?

It's a good idea but I would wait a bit for the initial implementation first and have more feedbacks about it.

@sheremet-va
Copy link
Member

I thought it was something possible with Jest, as you can nest describe blocks, it can make sense to have big groups to organize your tests (they don't really affect each other, it's more of an organization thing).

As long as they are in the same file, it should work. Spec files don't communicate with each other.

Yes maybe test|suite is too vague, how about single|group?

single|group is more vague :D

I am ok with suite, but not sure about test, since in my head test is a smallest block of this hierarchy.

As far as I understand, test scope fails the whole tests run?

@sheremet-va sheremet-va added the enhancement New feature or request label Jun 25, 2022
@jtuchel
Copy link

jtuchel commented Dec 13, 2022

May I ask if there is any news?

As already mentioned Jest provides a --bail flag
And Playwright provides a --max-failures -x flag

It would save a lot of time in CI pipelines :)

@vladcosorg
Copy link

I'm integrating vitest into the lint-staged pipeline and replacing jest. Would be cool to bail early if there is any failed test.

@prafed
Copy link

prafed commented Feb 24, 2023

Any update on this? I am quite happy with vitest but it can add up a lot of cost for CI pipelines if you get an error in the first minute of execution but subsequent tests keep on running for another 15 minutes.

@sheremet-va
Copy link
Member

Any update on this? I am quite happy with vitest but it can add up a lot of cost for CI pipelines if you get an error in the first minute of execution but subsequent tests keep on running for another 15 minutes.

PR welcome.

@trivikr

This comment was marked as outdated.

@trivikr
Copy link
Contributor

trivikr commented Feb 27, 2023

Is there any specific place folks can look at to introduce --bail option?

For example, here is a function which marks failure for a task in a runner:

function failTask(result: TaskResult, err: unknown) {
result.state = 'fail'
const error = processError(err)
result.error = error
result.errors ??= []
result.errors.push(error)
}

@trivikr

This comment was marked as outdated.

@trivikr
Copy link
Contributor

trivikr commented Feb 27, 2023

I think vitest runner needs to check value of suite.result.state after it's run, and break the for loop with failure if bail option is set.

Feel free to take up this task, once maintainers confirm the solution posted above is the right way to go ahead or provide alternative suggestions.

I'll revisit this issue after a month or so, and can take up if it's not implemented by then.

@trivikr

This comment was marked as outdated.

@trivikr
Copy link
Contributor

trivikr commented Feb 27, 2023

I think the check needs to be done in runTest function:

  • Track number of failed tests in a global variable (say numberOfFailedTests), if bail option is set.
  • Increment numberOfFailedTests whenever a test fails. It can be done whenever failTask is called on test.result. A better alternative would be to create a new function which does this work - say failTest which does this work and calls failTask
    • catch (e) {
      failTask(test.result, e)
      }
      try {
      await callSuiteHook(test.suite, test, 'afterEach', runner, [test.context, test.suite])
      await callCleanupHooks(beforeEachCleanups)
      }
      catch (e) {
      failTask(test.result, e)
      }
  • Skip running rest of the tests/suites if numberOfFailedTests is equal to or greater than one allowed by bail configuration.

If you're interested to take up this work, do wait for a comment from maintainers to check if there are better alternatives.

@sheremet-va
Copy link
Member

If you're interested to take up this work, do wait for a comment from maintainers to check if there are better alternatives.

I don't know. Probably right. Be aware that files will only have one file if it's running with --threads:

for (const file of files) {
if (!file.tasks.length && !runner.config.passWithNoTests) {

The hardest part here is to tell Vitest to stop running all other workers from within the worker without breaking Tinypool.

@trivikr
Copy link
Contributor

trivikr commented Feb 27, 2023

The hardest part here is to tell Vitest to stop running all other workers from within the worker without breaking Tinypool.

Can we use SharedArrayBuffer to store numberOfFailedTests so that it can be read and updated across workers?

The blog post Using Shared Array Buffer in Node.js provides an example of how SharedArrayBuffer can be shared between workers.

I see that birpc is used for communication between workers?

createBirpc<{}, RuntimeRPC>(
createMethodsRPC(ctx),
{
post(v) {
port.postMessage(v)
},
on(fn) {
port.on('message', fn)
},
},
)

Can this store numberOfFailedTests value, or some support needs to be added in tinypool?

cc Tinypool author @Aslemammad for comments.

@sheremet-va
Copy link
Member

Can we use SharedArrayBuffer to store numberOfFailedTests so that it can be read and updated across workers?

I think it's for use in worker_threads, but we also support child_process.

@trivikr
Copy link
Contributor

trivikr commented Feb 27, 2023

but we also support child_process.

The numberOfFailedTests can be tracked by sending messages between parent and child using subprocess.send.

@trivikr
Copy link
Contributor

trivikr commented Feb 27, 2023

It looks like the messages can be exchanged between worker threads or child processes using RuntimeRPC interfaced passed in createBirpc

@trivikr
Copy link
Contributor

trivikr commented Mar 1, 2023

After diving deep into the source code, I think the numberOfFailedTests can be tracked in Vitest core.

High level suggestion:

  • The variable can be similar to restartsCount (say failedTestsCount)
    restartsCount = 0
  • A function can be added (say onTestFailed) which tracks the failed tests count, and calls .exit(true) or equivalent when the number goes over what's configured by bail.
  • The interface RuntimeRPC provides a function to call core function.
  • The RuntimeRPC function is called from failTest in runner.

If you're interested to take up this work, do wait for a comment from maintainers to check if there are better alternatives.

@AriPerkkio
Copy link
Member

I think this and #3128 could share the mechanic of how test running is stopped. Maybe there should be a way to tell all pools (and their test runners) to stop their execution gracefully. And when a test fails with --bail option, or in #3128's case user manually attempts to pause the execution in watch mode, this signal would be sent to pools.

  • When a pool receives this signal it should not start a new test file. It should let the current test file finish properly so that all afterEach cleanups etc are run.
  • When a runner receives this signal it should not start a new test case. It should let the current test case finish. Maybe every test/it should check whether an abort signal has been sent earlier before starting up.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request pr welcome
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants