diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 2a25207227314f..7b5c00905eefbe 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -230,9 +230,8 @@ class Test extends AsyncResource { if (parent === null) { this.concurrency = 1; this.nesting = 0; - this.only = testOnlyFlag; this.reporter = new TestsStream(); - this.runOnlySubtests = this.only; + this.runOnlySubtests = testOnlyFlag; this.testNumber = 0; this.timeout = kDefaultTimeout; this.root = this; @@ -249,9 +248,8 @@ class Test extends AsyncResource { this.concurrency = parent.concurrency; this.nesting = nesting; - this.only = only ?? !parent.runOnlySubtests; this.reporter = parent.reporter; - this.runOnlySubtests = !this.only; + this.runOnlySubtests = false; this.testNumber = parent.subtests.length + 1; this.timeout = parent.timeout; this.root = parent.root; @@ -296,10 +294,6 @@ class Test extends AsyncResource { skip = 'test name does not match pattern'; } - if (testOnlyFlag && !this.only) { - skip = '\'only\' option not set'; - } - if (skip) { fn = noop; } @@ -338,10 +332,11 @@ class Test extends AsyncResource { this.subtests = []; this.waitingOn = 0; this.finished = false; + this.only = testOnlyFlag || parent?.runOnlySubtests ? only : undefined; - if (!testOnlyFlag && (only || this.runOnlySubtests)) { - const warning = - "'only' and 'runOnly' require the --test-only command-line option."; + + if (!testOnlyFlag && only && !parent.runOnlySubtests) { + const warning = "'only' requires the --test-only command-line option."; this.diagnostic(warning); } @@ -355,6 +350,18 @@ class Test extends AsyncResource { file: loc[2], }; } + + if (this.only && parent !== null) { + parent.markOnly(); + } + } + + markOnly() { + if (this.runOnlySubtests) { + return; + } + this.runOnlySubtests = true; + this.parent?.markOnly(); } matchesTestNamePatterns() { @@ -580,9 +587,18 @@ class Test extends AsyncResource { } } + get runOnlySibling() { + return this.parent?.runOnlySubtests && !this.only && !this.runOnlySubtests; + } + async run() { this.startTime = hrtime(); + if (this.runOnlySibling || this.only === false) { + this.fn = noop; + this.skip('\'only\' option not set'); + } + if (this[kShouldAbort]()) { this.postRun(); return; @@ -893,7 +909,6 @@ class Suite extends Test { this.fn = options.fn || this.fn; this.skipped = false; } - this.runOnlySubtests = testOnlyFlag; try { const { ctx, args } = this.getRunArgs(); @@ -915,7 +930,7 @@ class Suite extends Test { this.buildPhaseFinished = true; } - this.fn = () => {}; + this.fn = noop; } getRunArgs() { @@ -932,6 +947,7 @@ class Suite extends Test { async run() { const hookArgs = this.getRunArgs(); + this.runOnlySubtests ||= this.runOnlySibling; let stopPromise; try { diff --git a/test/fixtures/test-runner/output/only_tests.js b/test/fixtures/test-runner/output/only_tests.js index 26266b524454b7..cac152578dfc6b 100644 --- a/test/fixtures/test-runner/output/only_tests.js +++ b/test/fixtures/test-runner/output/only_tests.js @@ -1,100 +1,96 @@ // Flags: --test-only 'use strict'; -require('../../../common'); +const common = require('../../../common'); const { test, describe, it } = require('node:test'); // These tests should be skipped based on the 'only' option. -test('only = undefined'); -test('only = undefined, skip = string', { skip: 'skip message' }); -test('only = undefined, skip = true', { skip: true }); -test('only = undefined, skip = false', { skip: false }); -test('only = false', { only: false }); -test('only = false, skip = string', { only: false, skip: 'skip message' }); -test('only = false, skip = true', { only: false, skip: true }); -test('only = false, skip = false', { only: false, skip: false }); +test('only = undefined', common.mustNotCall()); +test('only = undefined, skip = string', { skip: 'skip message' }, common.mustNotCall()); +test('only = undefined, skip = true', { skip: true }, common.mustNotCall()); +test('only = undefined, skip = false', { skip: false }, common.mustNotCall()); +test('only = false', { only: false }, common.mustNotCall()); +test('only = false, skip = string', { only: false, skip: 'skip message' }, common.mustNotCall()); +test('only = false, skip = true', { only: false, skip: true }, common.mustNotCall()); +test('only = false, skip = false', { only: false, skip: false }, common.mustNotCall()); // These tests should be skipped based on the 'skip' option. -test('only = true, skip = string', { only: true, skip: 'skip message' }); -test('only = true, skip = true', { only: true, skip: true }); +test('only = true, skip = string', { only: true, skip: 'skip message' }, common.mustNotCall()); +test('only = true, skip = true', { only: true, skip: true }, common.mustNotCall()); // An 'only' test with subtests. -test('only = true, with subtests', { only: true }, async (t) => { +test('only = true, with subtests', { only: true }, common.mustCall(async (t) => { // These subtests should run. - await t.test('running subtest 1'); - await t.test('running subtest 2'); + await t.test('running subtest 1', common.mustCall()); + await t.test('running subtest 2', common.mustCall()); // Switch the context to only execute 'only' tests. t.runOnly(true); - await t.test('skipped subtest 1'); - await t.test('skipped subtest 2'); - await t.test('running subtest 3', { only: true }); + await t.test('skipped subtest 1', common.mustNotCall()); + await t.test('skipped subtest 2'), common.mustNotCall(); + await t.test('running subtest 3', { only: true }, common.mustCall()); // Switch the context back to execute all tests. t.runOnly(false); - await t.test('running subtest 4', async (t) => { + await t.test('running subtest 4', common.mustCall(async (t) => { // These subtests should run. - await t.test('running sub-subtest 1'); - await t.test('running sub-subtest 2'); + await t.test('running sub-subtest 1', common.mustCall()); + await t.test('running sub-subtest 2', common.mustCall()); // Switch the context to only execute 'only' tests. t.runOnly(true); - await t.test('skipped sub-subtest 1'); - await t.test('skipped sub-subtest 2'); - }); + await t.test('skipped sub-subtest 1', common.mustNotCall()); + await t.test('skipped sub-subtest 2', common.mustNotCall()); + })); // Explicitly do not run these tests. - await t.test('skipped subtest 3', { only: false }); - await t.test('skipped subtest 4', { skip: true }); -}); + await t.test('skipped subtest 3', { only: false }, common.mustNotCall()); + await t.test('skipped subtest 4', { skip: true }, common.mustNotCall()); +})); -describe.only('describe only = true, with subtests', () => { - it.only('`it` subtest 1 should run', () => {}); +describe.only('describe only = true, with subtests', common.mustCall(() => { + it.only('`it` subtest 1 should run', common.mustCall()); - it('`it` subtest 2 should not run', async () => {}); -}); + it('`it` subtest 2 should not run', common.mustNotCall()); +})); -describe.only('describe only = true, with a mixture of subtests', () => { - it.only('`it` subtest 1', () => {}); +describe.only('describe only = true, with a mixture of subtests', common.mustCall(() => { + it.only('`it` subtest 1', common.mustCall()); - it.only('`it` async subtest 1', async () => {}); + it.only('`it` async subtest 1', common.mustCall(async () => {})); - it('`it` subtest 2 only=true', { only: true }); + it('`it` subtest 2 only=true', { only: true }, common.mustCall()); - it('`it` subtest 2 only=false', { only: false }, () => { - throw new Error('This should not run'); - }); + it('`it` subtest 2 only=false', { only: false }, common.mustNotCall()); - it.skip('`it` subtest 3 skip', () => { - throw new Error('This should not run'); - }); + it.skip('`it` subtest 3 skip', common.mustNotCall()); - it.todo('`it` subtest 4 todo', { only: false }, () => { - throw new Error('This should not run'); - }); + it.todo('`it` subtest 4 todo', { only: false }, common.mustNotCall()); - test.only('`test` subtest 1', () => {}); + test.only('`test` subtest 1', common.mustCall()); - test.only('`test` async subtest 1', async () => {}); + test.only('`test` async subtest 1', common.mustCall(async () => {})); - test('`test` subtest 2 only=true', { only: true }); + test('`test` subtest 2 only=true', { only: true }, common.mustCall()); - test('`test` subtest 2 only=false', { only: false }, () => { - throw new Error('This should not run'); - }); + test('`test` subtest 2 only=false', { only: false }, common.mustNotCall()); - test.skip('`test` subtest 3 skip', () => { - throw new Error('This should not run'); - }); + test.skip('`test` subtest 3 skip', common.mustNotCall()); - test.todo('`test` subtest 4 todo', { only: false }, () => { - throw new Error('This should not run'); - }); -}); + test.todo('`test` subtest 4 todo', { only: false }, common.mustNotCall()); +})); -describe.only('describe only = true, with subtests', () => { - test.only('subtest should run', () => {}); +describe.only('describe only = true, with subtests', common.mustCall(() => { + test.only('subtest should run', common.mustCall()); - test('async subtest should not run', async () => {}); + test('async subtest should not run', common.mustNotCall()); - test('subtest should be skipped', { only: false }, () => {}); -}); + test('subtest should be skipped', { only: false }, common.mustNotCall()); +})); + +describe('describe only = undefined, with subtests', common.mustCall(() => { + test('async subtest should not run', common.mustNotCall()); +})); + +describe('describe only = false, with subtests', { only: false }, common.mustCall(() => { + test('async subtest should not run', common.mustNotCall()); +})); diff --git a/test/fixtures/test-runner/output/only_tests.snapshot b/test/fixtures/test-runner/output/only_tests.snapshot index ded19f3bec4c6a..de6251f29d9ef2 100644 --- a/test/fixtures/test-runner/output/only_tests.snapshot +++ b/test/fixtures/test-runner/output/only_tests.snapshot @@ -222,12 +222,36 @@ ok 14 - describe only = true, with subtests duration_ms: * type: 'suite' ... -1..14 -# tests 40 -# suites 3 +# Subtest: describe only = undefined, with subtests + # Subtest: async subtest should not run + ok 1 - async subtest should not run # SKIP 'only' option not set + --- + duration_ms: * + ... + 1..1 +ok 15 - describe only = undefined, with subtests + --- + duration_ms: * + type: 'suite' + ... +# Subtest: describe only = false, with subtests + # Subtest: async subtest should not run + ok 1 - async subtest should not run # SKIP 'only' option not set + --- + duration_ms: * + ... + 1..1 +ok 16 - describe only = false, with subtests + --- + duration_ms: * + type: 'suite' + ... +1..16 +# tests 42 +# suites 5 # pass 15 # fail 0 # cancelled 0 -# skipped 25 +# skipped 27 # todo 0 # duration_ms * diff --git a/test/fixtures/test-runner/output/output.js b/test/fixtures/test-runner/output/output.js index f37d3495030950..de7c220d7be595 100644 --- a/test/fixtures/test-runner/output/output.js +++ b/test/fixtures/test-runner/output/output.js @@ -274,11 +274,11 @@ test('callback async throw after done', (t, done) => { done(); }); -test('only is set but not in only mode', { only: true }, async (t) => { - // All of these subtests should run. +test('runOnly is set', async (t) => { + // Subtests should run only outside of a runOnly block, unless they have only: true. await t.test('running subtest 1'); t.runOnly(true); - await t.test('running subtest 2'); + await t.test('skipped subtest 2'); await t.test('running subtest 3', { only: true }); t.runOnly(false); await t.test('running subtest 4'); diff --git a/test/fixtures/test-runner/output/output.snapshot b/test/fixtures/test-runner/output/output.snapshot index 3a8ee2934389b7..cbd2ced1235f05 100644 --- a/test/fixtures/test-runner/output/output.snapshot +++ b/test/fixtures/test-runner/output/output.snapshot @@ -499,35 +499,32 @@ ok 48 - callback async throw after done --- duration_ms: * ... -# Subtest: only is set but not in only mode +# Subtest: runOnly is set # Subtest: running subtest 1 ok 1 - running subtest 1 --- duration_ms: * ... - # Subtest: running subtest 2 - ok 2 - running subtest 2 + # Subtest: skipped subtest 2 + ok 2 - skipped subtest 2 # SKIP 'only' option not set --- duration_ms: * ... - # 'only' and 'runOnly' require the --test-only command-line option. # Subtest: running subtest 3 ok 3 - running subtest 3 --- duration_ms: * ... - # 'only' and 'runOnly' require the --test-only command-line option. # Subtest: running subtest 4 ok 4 - running subtest 4 --- duration_ms: * ... 1..4 -ok 49 - only is set but not in only mode +ok 49 - runOnly is set --- duration_ms: * ... -# 'only' and 'runOnly' require the --test-only command-line option. # Subtest: custom inspect symbol fail not ok 50 - custom inspect symbol fail --- @@ -733,9 +730,9 @@ not ok 62 - invalid subtest fail # Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. # tests 76 # suites 0 -# pass 35 +# pass 34 # fail 25 # cancelled 3 -# skipped 9 +# skipped 10 # todo 4 # duration_ms * diff --git a/test/fixtures/test-runner/output/output_cli.snapshot b/test/fixtures/test-runner/output/output_cli.snapshot index 22da6e1d476ff7..e602dda9416590 100644 --- a/test/fixtures/test-runner/output/output_cli.snapshot +++ b/test/fixtures/test-runner/output/output_cli.snapshot @@ -499,35 +499,32 @@ ok 48 - callback async throw after done --- duration_ms: * ... -# Subtest: only is set but not in only mode +# Subtest: runOnly is set # Subtest: running subtest 1 ok 1 - running subtest 1 --- duration_ms: * ... - # Subtest: running subtest 2 - ok 2 - running subtest 2 + # Subtest: skipped subtest 2 + ok 2 - skipped subtest 2 # SKIP 'only' option not set --- duration_ms: * ... - # 'only' and 'runOnly' require the --test-only command-line option. # Subtest: running subtest 3 ok 3 - running subtest 3 --- duration_ms: * ... - # 'only' and 'runOnly' require the --test-only command-line option. # Subtest: running subtest 4 ok 4 - running subtest 4 --- duration_ms: * ... 1..4 -ok 49 - only is set but not in only mode +ok 49 - runOnly is set --- duration_ms: * ... -# 'only' and 'runOnly' require the --test-only command-line option. # Subtest: custom inspect symbol fail not ok 50 - custom inspect symbol fail --- @@ -738,9 +735,9 @@ ok 63 - last test 1..63 # tests 77 # suites 0 -# pass 36 +# pass 35 # fail 25 # cancelled 3 -# skipped 9 +# skipped 10 # todo 4 # duration_ms * diff --git a/test/fixtures/test-runner/output/spec_reporter.snapshot b/test/fixtures/test-runner/output/spec_reporter.snapshot index 3ceeee649bb99b..4511fa5b434243 100644 --- a/test/fixtures/test-runner/output/spec_reporter.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter.snapshot @@ -214,16 +214,13 @@ * callback async throw after done (*ms) - only is set but not in only mode + runOnly is set running subtest 1 (*ms) - running subtest 2 (*ms) - 'only' and 'runOnly' require the --test-only command-line option. + skipped subtest 2 (*ms) # 'only' option not set running subtest 3 (*ms) - 'only' and 'runOnly' require the --test-only command-line option. running subtest 4 (*ms) - only is set but not in only mode (*ms) + runOnly is set (*ms) - 'only' and 'runOnly' require the --test-only command-line option. custom inspect symbol fail (*ms) customized @@ -317,10 +314,10 @@ Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. tests 76 suites 0 - pass 35 + pass 34 fail 25 cancelled 3 - skipped 9 + skipped 10 todo 4 duration_ms * diff --git a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot index bb6c18d7b73dea..e78b1cf1a0df96 100644 --- a/test/fixtures/test-runner/output/spec_reporter_cli.snapshot +++ b/test/fixtures/test-runner/output/spec_reporter_cli.snapshot @@ -214,16 +214,13 @@ * callback async throw after done (*ms) - only is set but not in only mode + runOnly is set running subtest 1 (*ms) - running subtest 2 (*ms) - 'only' and 'runOnly' require the --test-only command-line option. + skipped subtest 2 (*ms) # 'only' option not set running subtest 3 (*ms) - 'only' and 'runOnly' require the --test-only command-line option. running subtest 4 (*ms) - only is set but not in only mode (*ms) + runOnly is set (*ms) - 'only' and 'runOnly' require the --test-only command-line option. custom inspect symbol fail (*ms) customized @@ -317,10 +314,10 @@ Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. tests 76 suites 0 - pass 35 + pass 34 fail 25 cancelled 3 - skipped 9 + skipped 10 todo 4 duration_ms *