diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 323ef2a25d434c..200d6df10a2751 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -1,5 +1,6 @@ 'use strict'; const { lstatSync, readdirSync } = require('fs'); +const { lstat, readdir } = require('fs/promises'); const { join, resolve } = require('path'); const { @@ -8,7 +9,9 @@ const { const { validateFunction, validateObject, + validateAbortSignal, } = require('internal/validators'); +const { AbortError } = require('internal/errors'); const { ArrayFrom, @@ -21,6 +24,8 @@ const { SafeMap, SafeSet, StringPrototypeEndsWith, + SymbolIterator, + SymbolAsyncIterator, } = primordials; let minimatch; @@ -51,6 +56,20 @@ class Cache { this.#statsCache.set(path, val); return val; } + async stat(path) { + const cached = this.#statsCache.get(path); + if (cached) { + return cached; + } + let val; + try { + val = await lstat(path); + } catch { + val = null; + } + this.#statsCache.set(path, val); + return val; + } addToStatCache(path, val) { this.#statsCache.set(path, val); } @@ -68,6 +87,22 @@ class Cache { this.#readdirCache.set(path, val); return val; } + + async readdir(path) { + const cached = this.#readdirCache.get(path); + if (cached) { + return cached; + } + let val; + try { + val = await readdir(path, { __proto__: null, withFileTypes: true }); + } catch { + val = []; + } + this.#readdirCache.set(path, val); + return val; + } + add(path, pattern) { let cache = this.#cache.get(path); if (!cache) { @@ -78,6 +113,7 @@ class Cache { pattern.indexes.forEach((index) => cache.add(pattern.cacheKey(index))); return cache.size !== originalSize + pattern.indexes.size; } + seen(path, pattern, index) { return this.#cache.get(path)?.has(pattern.cacheKey(index)); } @@ -148,15 +184,17 @@ class Glob { #root; #exclude; #cache = new Cache(); - #results = []; #queue = []; #subpatterns = new SafeMap(); constructor(patterns, options = kEmptyObject) { validateObject(options, 'options'); - const { exclude, cwd } = options; + const { exclude, cwd, signal } = options; if (exclude != null) { validateFunction(exclude, 'options.exclude'); } + if (signal != null) { + validateAbortSignal(options.signal, 'options.signal'); + } this.#root = cwd ?? '.'; this.#exclude = exclude; this.matchers = ArrayPrototypeMap(patterns, (pattern) => new (lazyMinimatch().Minimatch)(pattern, { @@ -169,32 +207,13 @@ class Glob { platform: process.platform, nocaseMagicOnly: true, })); + this.signal = signal; } globSync() { - ArrayPrototypePush(this.#queue, { - __proto__: null, - path: '.', - patterns: ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set, - (pattern, i) => new Pattern( - pattern, - matcher.globParts[i], - new SafeSet([0]), - new SafeSet(), - ))), - }); - - while (this.#queue.length > 0) { - const item = ArrayPrototypePop(this.#queue); - for (let i = 0; i < item.patterns.length; i++) { - this.#addSubpatterns(item.path, item.patterns[i]); - } - this.#subpatterns - .forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns })); - this.#subpatterns.clear(); - } - return this.#results; + return ArrayFrom(this); } + #addSubpattern(path, pattern) { if (!this.#subpatterns.has(path)) { this.#subpatterns.set(path, [pattern]); @@ -202,7 +221,8 @@ class Glob { ArrayPrototypePush(this.#subpatterns.get(path), pattern); } } - #addSubpatterns(path, pattern) { + + *#addSubpatternsSync(path, pattern) { const seen = this.#cache.add(path, pattern); if (seen) { return; @@ -240,16 +260,16 @@ class Glob { const p = pattern.at(-1); const stat = this.#cache.statSync(join(fullpath, p)); if (stat && (p || isDirectory)) { - ArrayPrototypePush(this.#results, join(path, p)); + yield join(path, p); } if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { return; } } else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR && - (path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) { + (path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) { // If pattern ends with **, add to results // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" - ArrayPrototypePush(this.#results, path); + yield path; } if (!isDirectory) { @@ -296,7 +316,7 @@ class Glob { subPatterns.add(index); } else if (!fromSymlink && index === last) { // If ** is last, add to results - ArrayPrototypePush(this.#results, entryPath); + yield entryPath; } // Any pattern after ** is also a potential pattern @@ -304,7 +324,7 @@ class Glob { const nextMatches = pattern.test(nextIndex, entry.name); if (nextMatches && nextIndex === last && !isLast) { // If next pattern is the last one, add to results - ArrayPrototypePush(this.#results, entryPath); + yield entryPath; } else if (nextMatches && entry.isDirectory()) { // Pattern mached, meaning two patterns forward // are also potential patterns @@ -312,7 +332,7 @@ class Glob { subPatterns.add(index + 2); } if ((nextMatches || pattern.at(0) === '.') && - (entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) { + (entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) { // If pattern after ** matches, or pattern starts with "." // and entry is a directory or symlink, add to potential patterns subPatterns.add(nextIndex); @@ -337,11 +357,11 @@ class Glob { } else { if (!this.#cache.seen(path, pattern, nextIndex)) { this.#cache.add(path, pattern.child(new SafeSet([nextIndex]))); - ArrayPrototypePush(this.#results, path); + yield path; } if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { this.#cache.add(parent, pattern.child(new SafeSet([nextIndex]))); - ArrayPrototypePush(this.#results, parent); + yield parent; } } } @@ -354,7 +374,7 @@ class Glob { } else if (current === '.' && pattern.test(nextIndex, entry.name)) { // If current pattern is ".", proceed to test next pattern if (nextIndex === last) { - ArrayPrototypePush(this.#results, entryPath); + yield entryPath; } else { subPatterns.add(nextIndex + 1); } @@ -364,7 +384,7 @@ class Glob { // If current pattern is a regex that matches entry name (e.g *.js) // add next pattern to potential patterns, or to results if it's the last pattern if (index === last) { - ArrayPrototypePush(this.#results, entryPath); + yield entryPath; } else if (entry.isDirectory()) { subPatterns.add(nextIndex); } @@ -376,6 +396,238 @@ class Glob { } } } + + async *#addSubpatterns(path, pattern) { + const seen = this.#cache.add(path, pattern); + if (seen) { + return; + } + const fullpath = resolve(this.#root, path); + const stat = await this.#cache.stat(fullpath); + const last = pattern.last; + const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks); + const isLast = pattern.isLast(isDirectory); + const isFirst = pattern.isFirst(); + + if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) { + // Absolute path, go to root + this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet([1]))); + return; + } + if (isFirst && pattern.at(0) === '') { + // Absolute path, go to root + this.#addSubpattern('/', pattern.child(new SafeSet([1]))); + return; + } + if (isFirst && pattern.at(0) === '..') { + // Start with .., go to parent + this.#addSubpattern('../', pattern.child(new SafeSet([1]))); + return; + } + if (isFirst && pattern.at(0) === '.') { + // Start with ., proceed + this.#addSubpattern('.', pattern.child(new SafeSet([1]))); + return; + } + + if (isLast && typeof pattern.at(-1) === 'string') { + // Add result if it exists + const p = pattern.at(-1); + const stat = await this.#cache.stat(join(fullpath, p)); + if (stat && (p || isDirectory)) { + yield join(path, p); + } + if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { + return; + } + } else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR && + (path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) { + // If pattern ends with **, add to results + // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" + yield path; + } + + if (!isDirectory) { + return; + } + + let children; + const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value); + if (typeof firstPattern === 'string') { + const stat = await this.#cache.stat(join(fullpath, firstPattern)); + if (stat) { + stat.name = firstPattern; + children = [stat]; + } else { + children = []; + } + } else { + children = await this.#cache.readdir(fullpath); + } + + for (let i = 0; i < children.length; i++) { + const entry = children[i]; + const entryPath = join(path, entry.name); + this.#cache.addToStatCache(join(fullpath, entry.name), entry); + + const subPatterns = new SafeSet(); + const nSymlinks = new SafeSet(); + for (const index of pattern.indexes) { + // For each child, chek potential patterns + if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { + return; + } + const current = pattern.at(index); + const nextIndex = index + 1; + const next = pattern.at(nextIndex); + const fromSymlink = pattern.symlinks.has(index); + + if (current === lazyMinimatch().GLOBSTAR) { + if (entry.name[0] === '.' || (this.#exclude && this.#exclude(entry.name))) { + continue; + } + if (!fromSymlink && entry.isDirectory()) { + // If directory, add ** to its potential patterns + subPatterns.add(index); + } else if (!fromSymlink && index === last) { + // If ** is last, add to results + yield entryPath; + } + + // Any pattern after ** is also a potential pattern + // so we can already test it here + const nextMatches = pattern.test(nextIndex, entry.name); + if (nextMatches && nextIndex === last && !isLast) { + // If next pattern is the last one, add to results + yield entryPath; + } else if (nextMatches && entry.isDirectory()) { + // Pattern mached, meaning two patterns forward + // are also potential patterns + // e.g **/b/c when entry is a/b - add c to potential patterns + subPatterns.add(index + 2); + } + if ((nextMatches || pattern.at(0) === '.') && + (entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) { + // If pattern after ** matches, or pattern starts with "." + // and entry is a directory or symlink, add to potential patterns + subPatterns.add(nextIndex); + } + + if (entry.isSymbolicLink()) { + nSymlinks.add(index); + } + + if (next === '..' && entry.isDirectory()) { + // In case pattern is "**/..", + // both parent and current directory should be added to the queue + // if this is the last pattern, add to results instead + const parent = join(path, '..'); + if (nextIndex < last) { + if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) { + this.#subpatterns.set(path, [pattern.child(new SafeSet([nextIndex + 1]))]); + } + if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) { + this.#subpatterns.set(parent, [pattern.child(new SafeSet([nextIndex + 1]))]); + } + } else { + if (!this.#cache.seen(path, pattern, nextIndex)) { + this.#cache.add(path, pattern.child(new SafeSet([nextIndex]))); + yield path; + } + if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { + this.#cache.add(parent, pattern.child(new SafeSet([nextIndex]))); + yield parent; + } + } + } + } + if (typeof current === 'string') { + if (pattern.test(index, entry.name) && index !== last) { + // If current pattern matches entry name + // the next pattern is a potential pattern + subPatterns.add(nextIndex); + } else if (current === '.' && pattern.test(nextIndex, entry.name)) { + // If current pattern is ".", proceed to test next pattern + if (nextIndex === last) { + yield entryPath; + } else { + subPatterns.add(nextIndex + 1); + } + } + } + if (typeof current === 'object' && pattern.test(index, entry.name)) { + // If current pattern is a regex that matches entry name (e.g *.js) + // add next pattern to potential patterns, or to results if it's the last pattern + if (index === last) { + yield entryPath; + } else if (entry.isDirectory()) { + subPatterns.add(nextIndex); + } + } + } + + if (subPatterns.size > 0) { + // If there are potential patterns, add to queue + this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks)); + } + } + } + + *[SymbolIterator]() { + ArrayPrototypePush(this.#queue, { + __proto__: null, + path: '.', + patterns: ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set, + (pattern, i) => new Pattern( + pattern, + matcher.globParts[i], + new SafeSet([0]), + new SafeSet(), + ))), + }); + + while (this.#queue.length > 0) { + const item = ArrayPrototypePop(this.#queue); + for (let i = 0; i < item.patterns.length; i++) { + yield *this.#addSubpatternsSync(item.path, item.patterns[i]); + } + this.#subpatterns + .forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns })); + this.#subpatterns.clear(); + } + } + + async *[SymbolAsyncIterator]() { + if (this.signal?.aborted) { + throw new AbortError(); + } + + ArrayPrototypePush(this.#queue, { + __proto__: null, + path: '.', + patterns: ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set, + (pattern, i) => new Pattern( + pattern, + matcher.globParts[i], + new SafeSet([0]), + new SafeSet(), + ))), + }); + + while (this.#queue.length > 0) { + const item = ArrayPrototypePop(this.#queue); + for (let i = 0; i < item.patterns.length; i++) { + if (this.signal?.aborted) { + throw new AbortError(); + } + + yield* await this.#addSubpatterns(item.path, item.patterns[i]); + } + this.#subpatterns + .forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns })); + this.#subpatterns.clear(); + } + } } module.exports = { diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index f150a8f5ed85c2..e3ab5711453d88 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -152,7 +152,7 @@ function setup(root) { const terminationHandler = async () => { await exitHandler(); - process.exit(); + process.exit(1); }; process.on('uncaughtException', exceptionHandler); diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index af121273c9e638..45d9f7eed31516 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -11,7 +11,6 @@ const { ArrayPrototypeShift, ArrayPrototypeSlice, ArrayPrototypeSome, - ArrayPrototypeSort, ObjectAssign, PromisePrototypeThen, SafePromiseAll, @@ -25,10 +24,13 @@ const { StringPrototypeStartsWith, TypedArrayPrototypeGetLength, TypedArrayPrototypeSubarray, + NumberMAX_SAFE_INTEGER, } = primordials; const { spawn } = require('child_process'); +const { Readable } = require('stream'); const { finished } = require('internal/streams/end-of-stream'); +const { isNodeStream } = require('internal/streams/utils'); const { DefaultDeserializer, DefaultSerializer } = require('v8'); // TODO(aduh95): switch to internal/readline/interface when backporting to Node.js 16.x is no longer a concern. const { createInterface } = require('readline'); @@ -87,14 +89,23 @@ function createTestFileList() { cwd, exclude: (name) => name === 'node_modules', }); - const results = glob.globSync(); - if (hasUserSuppliedPattern && results.length === 0 && ArrayPrototypeEvery(glob.matchers, (m) => !m.hasMagic())) { - console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`); - process.exit(kGenericUserError); + if (hasUserSuppliedPattern && ArrayPrototypeEvery(glob.matchers, (m) => !m.hasMagic())) { + return Readable.from((async function*() { + let hasFile = false; + for await (const file of glob) { + hasFile = true; + yield file; + } + + if (!hasFile) { + console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`); + process.exit(kGenericUserError); + } + })()); } - return ArrayPrototypeSort(results); + return Readable.from(glob); } function filterExecArgv(arg, i, arr) { @@ -373,7 +384,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { return subtest.start(); } -function watchFiles(testFiles, root, inspectPort, signal, testNamePatterns) { +function watchFiles(testFilesPromise, root, inspectPort, signal, testNamePatterns) { const runningProcesses = new SafeMap(); const runningSubtests = new SafeMap(); const watcher = new FilesWatcher({ throttle: 500, mode: 'filter', signal }); @@ -381,7 +392,7 @@ function watchFiles(testFiles, root, inspectPort, signal, testNamePatterns) { watcher.on('changed', ({ owners }) => { watcher.unfilterFilesOwnedBy(owners); - PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => { + PromisePrototypeThen(testFilesPromise, (testFiles) => SafePromiseAllReturnVoid(testFiles, async (file) => { if (!owners.has(file)) { return; } @@ -446,21 +457,35 @@ function run(options) { } const root = createTestTree({ concurrency, timeout, signal }); - const testFiles = files ?? createTestFileList(); + let testFiles = files ?? createTestFileList(); let postRun = () => root.postRun(); let filesWatcher; if (watch) { + // Overriding testFiles so we won't try to consume again the stream + testFiles = ArrayIsArray(testFiles) ? testFiles : testFiles.toArray(); + + // TODO - should not pass a promise filesWatcher = watchFiles(testFiles, root, inspectPort, signal, testNamePatterns); postRun = undefined; } - const runFiles = () => { + + const runSingleFile = (path) => { + const subtest = runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns); + filesWatcher?.runningSubtests.set(path, subtest); + return subtest; + }; + + const runFiles = async () => { root.harness.bootstrapComplete = true; - return SafePromiseAllSettledReturnVoid(testFiles, (path) => { - const subtest = runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns); - filesWatcher?.runningSubtests.set(path, subtest); - return subtest; - }); + + if (isNodeStream(testFiles)) { + return testFiles + .map(runSingleFile, { __proto__: null, signal, concurrency: NumberMAX_SAFE_INTEGER }) + .toArray(); + } + + return SafePromiseAllSettledReturnVoid(await testFiles, runSingleFile); }; PromisePrototypeThen(PromisePrototypeThen(PromiseResolve(setup?.(root)), runFiles), postRun); diff --git a/test/parallel/test-fs-glob.mjs b/test/parallel/test-fs-glob.mjs index b1420fec272923..773ee6763d6186 100644 --- a/test/parallel/test-fs-glob.mjs +++ b/test/parallel/test-fs-glob.mjs @@ -300,10 +300,96 @@ const patterns = { ], }; + +test('Glob should be an iterator', () => { + const globInstance = new glob.Glob(['a/b/**'], { cwd: fixtureDir }); + + assert.strictEqual( + typeof globInstance[Symbol.iterator], + 'function', + new TypeError('Glob instance should be iterable') + ); +}); + +test('Glob should be an async iterator', () => { + const globInstance = new glob.Glob(['a/b/**'], { cwd: fixtureDir }); + + assert.strictEqual( + typeof globInstance[Symbol.asyncIterator], + 'function', + new TypeError('Glob instance should be iterable') + ); +}); + +test('should throw an abort error when signal is already aborted', async () => { + const ac = new AbortController(); + + const actual = new glob.Glob(['a/b/**'], { cwd: fixtureDir, signal: ac.signal }); + + ac.abort(); + try { + // eslint-disable-next-line no-unused-vars + for await (const _ of actual) { + assert.strictEqual('', 'should not get any item'); + } + assert.strictEqual('', 'unreachable'); + } catch (err) { + assert.strictEqual(err.name, 'AbortError'); + } +}); + +test('should not continue after aborting', async () => { + const ac = new AbortController(); + const pattern = '**/a/**'; + + const actual = new glob.Glob([pattern], { cwd: fixtureDir, signal: ac.signal }); + + const matches = []; + try { + for await (const match of actual) { + matches.push(match); + + if (matches.length === 2) { + ac.abort(); + } + + if (matches.length > 2) { + assert.strictEqual('', 'should not get any more items after aborting'); + } + } + assert.strictEqual('', 'unreachable'); + } catch (err) { + assert.strictEqual(err.name, 'AbortError'); + } +}); + for (const [pattern, expected] of Object.entries(patterns)) { - test(pattern, () => { + test(`${pattern} globSync`, () => { const actual = new glob.Glob([pattern], { cwd: fixtureDir }).globSync().sort(); const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort(); assert.deepStrictEqual(actual, normalized); }); + + test(`${pattern} iterable`, () => { + const globInstance = new glob.Glob([pattern], { cwd: fixtureDir }); + const actual = Array.from(globInstance).sort(); + const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort(); + + assert.deepStrictEqual(actual, normalized); + }); + + test(`${pattern} async iterable`, async () => { + const actual = new glob.Glob([pattern], { cwd: fixtureDir }); + + const normalizedArr = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)); + const normalized = new Set(normalizedArr); + + let matchesCount = 0; + for await (const matchItem of actual) { + matchesCount++; + assert.ok(normalized.has(matchItem), new Error(`${matchItem} not suppose to be in the glob matches (should be one of ${normalizedArr})`)); + } + + assert.strictEqual(normalized.size, matchesCount); + }); } diff --git a/test/parallel/test-runner-cli.js b/test/parallel/test-runner-cli.js index 704e72b2df49d6..47bda19d827736 100644 --- a/test/parallel/test-runner-cli.js +++ b/test/parallel/test-runner-cli.js @@ -14,7 +14,7 @@ const testFixtures = fixtures.path('test-runner'); assert.strictEqual(child.status, 1); assert.strictEqual(child.signal, null); - assert.strictEqual(child.stdout.toString(), ''); + assert.strictEqual(child.stdout.toString(), 'TAP version 13\n'); assert.match(child.stderr.toString(), /^Could not find/); } @@ -28,10 +28,12 @@ const testFixtures = fixtures.path('test-runner'); assert.strictEqual(child.signal, null); assert.strictEqual(child.stderr.toString(), ''); const stdout = child.stdout.toString(); + assert.match(stdout, /ok 1 - this should pass/); assert.match(stdout, /not ok 2 - this should fail/); - assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/); - assert.match(stdout, /ok 4 - this should pass/); + assert.match(stdout, /ok 3 - this should pass/); + assert.match(stdout, /ok 4 - this should be skipped/); + assert.match(stdout, /ok 5 - subdir.+subdir_test\.js/); } { @@ -42,8 +44,9 @@ const testFixtures = fixtures.path('test-runner'); const stdout = child.stdout.toString(); assert.match(stdout, /ok 1 - this should pass/); assert.match(stdout, /not ok 2 - this should fail/); - assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/); - assert.match(stdout, /ok 4 - this should pass/); + assert.match(stdout, /ok 3 - this should pass/); + assert.match(stdout, /ok 4 - this should be skipped/); + assert.match(stdout, /ok 5 - subdir.+subdir_test\.js/); assert.strictEqual(child.status, 1); assert.strictEqual(child.signal, null); assert.strictEqual(child.stderr.toString(), ''); @@ -85,8 +88,9 @@ const testFixtures = fixtures.path('test-runner'); const stdout = child.stdout.toString(); assert.match(stdout, /ok 1 - this should pass/); assert.match(stdout, /not ok 2 - this should fail/); - assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/); - assert.match(stdout, /ok 4 - this should pass/); + assert.match(stdout, /ok 3 - this should pass/); + assert.match(stdout, /ok 4 - this should be skipped/); + assert.match(stdout, /ok 5 - subdir.+subdir_test\.js/); } { @@ -124,16 +128,13 @@ const testFixtures = fixtures.path('test-runner'); assert.strictEqual(child.signal, null); assert.strictEqual(child.stderr.toString(), ''); const stdout = child.stdout.toString(); + assert.match(stdout, /# Subtest: this should pass/); assert.match(stdout, /ok 1 - this should pass/); assert.match(stdout, / {2}---/); assert.match(stdout, / {2}duration_ms: .*/); assert.match(stdout, / {2}\.\.\./); - assert.match(stdout, /# Subtest: .+invalid-tap\.js/); - assert.match(stdout, /# invalid tap output/); - assert.match(stdout, /ok 2 - .+invalid-tap\.js/); - assert.match(stdout, /# Subtest: level 0a/); assert.match(stdout, / {4}# Subtest: level 1a/); assert.match(stdout, / {4}ok 1 - level 1a/); @@ -146,11 +147,15 @@ const testFixtures = fixtures.path('test-runner'); assert.match(stdout, / {4}ok 3 - level 1c # SKIP aaa/); assert.match(stdout, / {4}# Subtest: level 1d/); assert.match(stdout, / {4}ok 4 - level 1d/); - assert.match(stdout, /not ok 3 - level 0a/); + assert.match(stdout, /not ok 2 - level 0a/); assert.match(stdout, / {2}error: '1 subtest failed'/); assert.match(stdout, /# Subtest: level 0b/); - assert.match(stdout, /not ok 4 - level 0b/); + assert.match(stdout, /not ok 3 - level 0b/); assert.match(stdout, / {2}error: 'level 0b error'/); + + assert.match(stdout, /# Subtest: .+invalid-tap\.js/); + assert.match(stdout, /# invalid tap output/); + assert.match(stdout, /ok 3 - .+invalid-tap\.js/); assert.match(stdout, /# tests 8/); assert.match(stdout, /# pass 4/); assert.match(stdout, /# fail 3/); diff --git a/test/parallel/test-runner-coverage.js b/test/parallel/test-runner-coverage.js index 9377f1bb509328..9b471a6772a71b 100644 --- a/test/parallel/test-runner-coverage.js +++ b/test/parallel/test-runner-coverage.js @@ -89,7 +89,7 @@ test('test tap coverage reporter', skipIfNoInspector, async (t) => { const result = spawnSync(process.execPath, args, options); const report = getTapCoverageFixtureReport(); - assert(result.stdout.toString().includes(report)); + assert.ok(result.stdout.toString().includes(report), new Error(`test stdout should include:\n${report}\n\nBut instead got:\n${result.stdout.toString()}`)); assert.strictEqual(result.stderr.toString(), ''); assert.strictEqual(result.status, 0); assert(findCoverageFileForPid(result.pid));