From 041a41c52906a9ea9c7b8458afb1d35affd6e9fe Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 11 Sep 2023 12:46:34 -0700 Subject: [PATCH 01/16] Expose internal events for custom reporters --- lib/cli.js | 18 ++++++++-- test/internal-events/fixtures/.gitignore | 1 + test/internal-events/fixtures/ava.config.js | 16 +++++++++ test/internal-events/fixtures/package.json | 3 ++ test/internal-events/fixtures/test.js | 5 +++ test/internal-events/test.js | 38 +++++++++++++++++++++ 6 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 test/internal-events/fixtures/.gitignore create mode 100644 test/internal-events/fixtures/ava.config.js create mode 100644 test/internal-events/fixtures/package.json create mode 100644 test/internal-events/fixtures/test.js create mode 100644 test/internal-events/test.js diff --git a/lib/cli.js b/lib/cli.js index 002a40b6a..5da3ed685 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -471,10 +471,24 @@ export default async function loadCli() { // eslint-disable-line complexity }); } - api.on('run', plan => { + api.on('run', async plan => { + if (combined.onInternalEvent) { + await combined.onInternalEvent({ + type: 'run', + plan, + }); + } + reporter.startRun(plan); - plan.status.on('stateChange', evt => { + plan.status.on('stateChange', async evt => { + if (combined.onInternalEvent) { + await combined.onInternalEvent({ + type: 'stateChange', + stateChange: evt, + }); + } + if (evt.type === 'end' || evt.type === 'interrupt') { // Write out code coverage data when the run ends, lest a process // interrupt causes it to be lost. diff --git a/test/internal-events/fixtures/.gitignore b/test/internal-events/fixtures/.gitignore new file mode 100644 index 000000000..1fe1da7f5 --- /dev/null +++ b/test/internal-events/fixtures/.gitignore @@ -0,0 +1 @@ +internal-events.json diff --git a/test/internal-events/fixtures/ava.config.js b/test/internal-events/fixtures/ava.config.js new file mode 100644 index 000000000..b0634d2db --- /dev/null +++ b/test/internal-events/fixtures/ava.config.js @@ -0,0 +1,16 @@ +import fs from 'node:fs/promises'; + +const internalEvents = []; + +export default { + files: [ + 'test.js', + ], + async onInternalEvent(event) { + internalEvents.push(event); + + if (event.type === 'stateChange' && event.stateChange.type === 'end') { + await fs.writeFile('internal-events.json', JSON.stringify(internalEvents)); + } + }, +}; diff --git a/test/internal-events/fixtures/package.json b/test/internal-events/fixtures/package.json new file mode 100644 index 000000000..bedb411a9 --- /dev/null +++ b/test/internal-events/fixtures/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/internal-events/fixtures/test.js b/test/internal-events/fixtures/test.js new file mode 100644 index 000000000..0fd3dbd7e --- /dev/null +++ b/test/internal-events/fixtures/test.js @@ -0,0 +1,5 @@ +import test from 'ava'; + +test('placeholder', t => { + t.pass(); +}); diff --git a/test/internal-events/test.js b/test/internal-events/test.js new file mode 100644 index 000000000..60cd73441 --- /dev/null +++ b/test/internal-events/test.js @@ -0,0 +1,38 @@ +import fs from 'node:fs/promises'; +import {fileURLToPath} from 'node:url'; + +import test from '@ava/test'; + +import {fixture} from '../helpers/exec.js'; + +test('internal events are emitted', async t => { + await fixture(); + + const result = JSON.parse(await fs.readFile(fileURLToPath(new URL('fixtures/internal-events.json', import.meta.url)))); + + t.like(result[0], { + type: 'run', + plan: { + files: [ + fileURLToPath(new URL('fixtures/test.js', import.meta.url)), + ], + }, + }); + + const testPassedEvent = result.find(event => event.type === 'stateChange' && event.stateChange.type === 'test-passed'); + t.like(testPassedEvent, { + type: 'stateChange', + stateChange: { + type: 'test-passed', + title: 'placeholder', + testFile: fileURLToPath(new URL('fixtures/test.js', import.meta.url)), + }, + }); + + t.like(result[result.length - 1], { + type: 'stateChange', + stateChange: { + type: 'end', + }, + }); +}); From 84dba1d76a8035bbacfb3ad01c489e7408e13d6b Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 25 Sep 2023 11:51:01 -0700 Subject: [PATCH 02/16] Address comment from review --- lib/cli.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 5da3ed685..14b63fadd 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -472,12 +472,10 @@ export default async function loadCli() { // eslint-disable-line complexity } api.on('run', async plan => { - if (combined.onInternalEvent) { - await combined.onInternalEvent({ - type: 'run', - plan, - }); - } + combined.onInternalEvent?.({ + type: 'run', + plan, + }); reporter.startRun(plan); From f174798d30c9b2bbba83e5330d2667417b23aee0 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 25 Sep 2023 12:16:51 -0700 Subject: [PATCH 03/16] Use async generator for events --- lib/cli.js | 15 +++---------- lib/external-run-observer.js | 25 +++++++++++++++++++++ test/internal-events/fixtures/ava.config.js | 10 +++++---- 3 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 lib/external-run-observer.js diff --git a/lib/cli.js b/lib/cli.js index 14b63fadd..88cc9f0b7 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -19,6 +19,7 @@ import {loadConfig} from './load-config.js'; import normalizeModuleTypes from './module-types.js'; import normalizeNodeArguments from './node-arguments.js'; import pkg from './pkg.cjs'; +import { ExternalRunObserver } from './external-run-observer.js'; function exit(message) { console.error(`\n ${chalk.red(figures.cross)} ${message}`); @@ -471,22 +472,12 @@ export default async function loadCli() { // eslint-disable-line complexity }); } - api.on('run', async plan => { - combined.onInternalEvent?.({ - type: 'run', - plan, - }); + const externalRunObserver = combined.observeRun ? new ExternalRunObserver(api, combined.observeRun) : null; + api.on('run', async plan => { reporter.startRun(plan); plan.status.on('stateChange', async evt => { - if (combined.onInternalEvent) { - await combined.onInternalEvent({ - type: 'stateChange', - stateChange: evt, - }); - } - if (evt.type === 'end' || evt.type === 'interrupt') { // Write out code coverage data when the run ends, lest a process // interrupt causes it to be lost. diff --git a/lib/external-run-observer.js b/lib/external-run-observer.js new file mode 100644 index 000000000..783ee60c3 --- /dev/null +++ b/lib/external-run-observer.js @@ -0,0 +1,25 @@ +import { on } from "node:events" + +export class ExternalRunObserver { + constructor(api, externalObserveHandler) { + externalObserveHandler({ + events: this.eventsFromApi(api) + }) + } + + async * eventsFromApi(api) { + for await (const [plan] of on(api, 'run')) { + yield { + type: 'run', + plan + }; + + for await (const [stateChange] of on(plan.status, 'stateChange')) { + yield { + type: 'stateChange', + stateChange + } + } + } + } +} diff --git a/test/internal-events/fixtures/ava.config.js b/test/internal-events/fixtures/ava.config.js index b0634d2db..6d39c4d33 100644 --- a/test/internal-events/fixtures/ava.config.js +++ b/test/internal-events/fixtures/ava.config.js @@ -6,11 +6,13 @@ export default { files: [ 'test.js', ], - async onInternalEvent(event) { - internalEvents.push(event); + async observeRun(run) { + for await (const event of run.events) { + internalEvents.push(event); - if (event.type === 'stateChange' && event.stateChange.type === 'end') { - await fs.writeFile('internal-events.json', JSON.stringify(internalEvents)); + if (event.type === 'stateChange' && event.stateChange.type === 'end') { + await fs.writeFile('internal-events.json', JSON.stringify(internalEvents)); + } } }, }; From 74eee6774171f9c3b53f111f340eca661c5b8fba Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 25 Sep 2023 12:19:17 -0700 Subject: [PATCH 04/16] Fix lint issues --- lib/external-run-observer.js | 40 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/external-run-observer.js b/lib/external-run-observer.js index 783ee60c3..87156a962 100644 --- a/lib/external-run-observer.js +++ b/lib/external-run-observer.js @@ -1,25 +1,25 @@ -import { on } from "node:events" +import {on} from 'node:events' export class ExternalRunObserver { - constructor(api, externalObserveHandler) { - externalObserveHandler({ - events: this.eventsFromApi(api) - }) - } + constructor(api, externalObserveHandler) { + externalObserveHandler({ + events: this.eventsFromApi(api) + }); + } - async * eventsFromApi(api) { - for await (const [plan] of on(api, 'run')) { - yield { - type: 'run', - plan - }; + async * eventsFromApi(api) { + for await (const [plan] of on(api, 'run')) { + yield { + type: 'run', + plan + }; - for await (const [stateChange] of on(plan.status, 'stateChange')) { - yield { - type: 'stateChange', - stateChange - } - } - } - } + for await (const [stateChange] of on(plan.status, 'stateChange')) { + yield { + type: 'stateChange', + stateChange + } + } + } + } } From 92969447254557f272abce957f1e7d3edc62c2e5 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 25 Sep 2023 12:27:29 -0700 Subject: [PATCH 05/16] No need for class --- lib/api-event-iterator.js | 17 +++++++++++++++++ lib/cli.js | 8 ++++++-- lib/external-run-observer.js | 25 ------------------------- 3 files changed, 23 insertions(+), 27 deletions(-) create mode 100644 lib/api-event-iterator.js delete mode 100644 lib/external-run-observer.js diff --git a/lib/api-event-iterator.js b/lib/api-event-iterator.js new file mode 100644 index 000000000..3ceb252fb --- /dev/null +++ b/lib/api-event-iterator.js @@ -0,0 +1,17 @@ +import {on} from 'node:events'; + +export async function * asyncEventIteratorFromApi(api) { + for await (const [plan] of on(api, 'run')) { + yield { + type: 'run', + plan, + }; + + for await (const [stateChange] of on(plan.status, 'stateChange')) { + yield { + type: 'stateChange', + stateChange, + }; + } + } +} diff --git a/lib/cli.js b/lib/cli.js index 88cc9f0b7..6841bc5f6 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -8,6 +8,7 @@ import figures from 'figures'; import yargs from 'yargs'; import {hideBin} from 'yargs/helpers'; // eslint-disable-line n/file-extension-in-import +import {asyncEventIteratorFromApi} from './api-event-iterator.js'; import Api from './api.js'; import {chalk} from './chalk.js'; import validateEnvironmentVariables from './environment-variables.js'; @@ -19,7 +20,6 @@ import {loadConfig} from './load-config.js'; import normalizeModuleTypes from './module-types.js'; import normalizeNodeArguments from './node-arguments.js'; import pkg from './pkg.cjs'; -import { ExternalRunObserver } from './external-run-observer.js'; function exit(message) { console.error(`\n ${chalk.red(figures.cross)} ${message}`); @@ -472,7 +472,11 @@ export default async function loadCli() { // eslint-disable-line complexity }); } - const externalRunObserver = combined.observeRun ? new ExternalRunObserver(api, combined.observeRun) : null; + if (combined.observeRun) { + combined.observeRun({ + events: asyncEventIteratorFromApi(api), + }); + } api.on('run', async plan => { reporter.startRun(plan); diff --git a/lib/external-run-observer.js b/lib/external-run-observer.js deleted file mode 100644 index 87156a962..000000000 --- a/lib/external-run-observer.js +++ /dev/null @@ -1,25 +0,0 @@ -import {on} from 'node:events' - -export class ExternalRunObserver { - constructor(api, externalObserveHandler) { - externalObserveHandler({ - events: this.eventsFromApi(api) - }); - } - - async * eventsFromApi(api) { - for await (const [plan] of on(api, 'run')) { - yield { - type: 'run', - plan - }; - - for await (const [stateChange] of on(plan.status, 'stateChange')) { - yield { - type: 'stateChange', - stateChange - } - } - } - } -} From 7b0c55b183a76e18f1ffdccec6febb3c8a9194d8 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 25 Sep 2023 12:31:01 -0700 Subject: [PATCH 06/16] Gate behind nonSemVerExperiments --- lib/cli.js | 2 +- lib/load-config.js | 2 +- test/internal-events/fixtures/ava.config.js | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 6841bc5f6..e8d61fbd1 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -472,7 +472,7 @@ export default async function loadCli() { // eslint-disable-line complexity }); } - if (combined.observeRun) { + if (combined.observeRun && experiments.observeRunsFromConfig) { combined.observeRun({ events: asyncEventIteratorFromApi(api), }); diff --git a/lib/load-config.js b/lib/load-config.js index 9994629a6..9aa8eb079 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -8,7 +8,7 @@ import {packageConfig, packageJsonPath} from 'pkg-conf'; const NO_SUCH_FILE = Symbol('no ava.config.js file'); const MISSING_DEFAULT_EXPORT = Symbol('missing default export'); -const EXPERIMENTS = new Set(); +const EXPERIMENTS = new Set(['observeRunsFromConfig']); const importConfig = async ({configFile, fileForErrorMessage}) => { const {default: config = MISSING_DEFAULT_EXPORT} = await import(url.pathToFileURL(configFile)); diff --git a/test/internal-events/fixtures/ava.config.js b/test/internal-events/fixtures/ava.config.js index 6d39c4d33..62066753a 100644 --- a/test/internal-events/fixtures/ava.config.js +++ b/test/internal-events/fixtures/ava.config.js @@ -6,6 +6,9 @@ export default { files: [ 'test.js', ], + nonSemVerExperiments: { + observeRunsFromConfig: true, + }, async observeRun(run) { for await (const event of run.events) { internalEvents.push(event); From 78f9d371f99b45a9633cb629b4bac8862ed2dbed Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 25 Sep 2023 14:54:45 -0700 Subject: [PATCH 07/16] Add types --- entrypoints/internal.cjs | 1 + entrypoints/internal.d.cts | 1 + entrypoints/internal.d.mts | 1 + entrypoints/internal.d.ts | 25 ++++++ entrypoints/internal.mjs | 1 + internal.d.ts | 3 + package.json | 10 +++ types/state-change-events.d.cts | 143 ++++++++++++++++++++++++++++++++ 8 files changed, 185 insertions(+) create mode 100644 entrypoints/internal.cjs create mode 100644 entrypoints/internal.d.cts create mode 100644 entrypoints/internal.d.mts create mode 100644 entrypoints/internal.d.ts create mode 100644 entrypoints/internal.mjs create mode 100644 internal.d.ts create mode 100644 types/state-change-events.d.cts diff --git a/entrypoints/internal.cjs b/entrypoints/internal.cjs new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/entrypoints/internal.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/entrypoints/internal.d.cts b/entrypoints/internal.d.cts new file mode 100644 index 000000000..09f74b9a2 --- /dev/null +++ b/entrypoints/internal.d.cts @@ -0,0 +1 @@ +export * from "./internal" diff --git a/entrypoints/internal.d.mts b/entrypoints/internal.d.mts new file mode 100644 index 000000000..09f74b9a2 --- /dev/null +++ b/entrypoints/internal.d.mts @@ -0,0 +1 @@ +export * from "./internal" diff --git a/entrypoints/internal.d.ts b/entrypoints/internal.d.ts new file mode 100644 index 000000000..b3392c4a3 --- /dev/null +++ b/entrypoints/internal.d.ts @@ -0,0 +1,25 @@ +import type {StateChangeEvent} from '../types/state-change-events.d.cts'; + +export type RunEvent = { + type: 'stateChange'; + stateChange: StateChangeEvent; +} | { + type: 'run'; + plan: { + bailWithoutReporting: boolean; + debug: boolean; + failFastEnabled: boolean; + filePathPrefix: string; + files: string[]; + matching: boolean; + previousFailures: number; + runOnlyExclusive: boolean; + firstRun: boolean; + }; +}; + +export type {StateChangeEvent} from '../types/state-change-events.d.cts'; + +export type Run = { + events: AsyncIterableIterator; +}; diff --git a/entrypoints/internal.mjs b/entrypoints/internal.mjs new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/entrypoints/internal.mjs @@ -0,0 +1 @@ +export {}; diff --git a/internal.d.ts b/internal.d.ts new file mode 100644 index 000000000..7971cabce --- /dev/null +++ b/internal.d.ts @@ -0,0 +1,3 @@ +// For compatibility with resolution algorithms other than Node16. + +export * from './entrypoints/internal.cjs'; diff --git a/package.json b/package.json index 5f124ff64..88c04fdce 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,16 @@ "types": "./entrypoints/plugin.d.cts", "default": "./entrypoints/plugin.cjs" } + }, + "./internal": { + "import": { + "types": "./entrypoints/internal.d.mts", + "default": "./entrypoints/internal.mjs" + }, + "require": { + "types": "./entrypoints/internal.d.cts", + "default": "./entrypoints/internal.cjs" + } } }, "type": "module", diff --git a/types/state-change-events.d.cts b/types/state-change-events.d.cts new file mode 100644 index 000000000..fd481b2fa --- /dev/null +++ b/types/state-change-events.d.cts @@ -0,0 +1,143 @@ +type ErrorSource = { + isDependency: boolean + isWithinProject: boolean + file: string + line: number +} + +type SerializedErrorBase = { + message: string + name: string, + originalError: unknown, + stack: string +} + +type AggregateSerializedError = SerializedErrorBase & { + type: "aggregate" + errors: SerializedError[] +} + +type NativeSerializedError = SerializedErrorBase & { + type: "native" + source: ErrorSource | null +} + +type AVASerializedError = SerializedErrorBase & { + type: "ava" + assertion: string + improperUsage: unknown | null + formattedCause: unknown | null + formattedDetails: unknown | unknown[] + source: ErrorSource | null +} + +type SerializedError = AggregateSerializedError | NativeSerializedError | AVASerializedError + +export type StateChangeEvent = { + type: "starting", + testFile: string +} | { + type: "stats", + stats: { + byFile: Map + declaredTests: number + failedHooks: number, + failedTests: number, + failedWorkers: number, + files: number, + parallelRuns: { + currentIndex: number, + totalRuns: number + } | null + finishedWorkers: number, + internalErrors: number + remainingTests: number, + passedKnownFailingTests: number, + passedTests: number, + selectedTests: number, + sharedWorkerErrors: number, + skippedTests: number, + timedOutTests: number, + timeouts: number, + todoTests: number, + uncaughtExceptions: number, + unhandledRejections: number, + } +} | { + type: "declared-test" + title: string + knownFailing: boolean + todo: boolean + testFile: string +} | { + type: "selected-test" + title: string + knownFailing: boolean + skip: boolean + todo: boolean + testFile: string +} | { + type: "test-register-log-reference" + title: string + logs: string[] + testFile: string +} | { + type: "test-passed", + title: string + duration: number + knownFailing: boolean + logs: string[] + testFile: string +} | { + type: "test-failed", + title: string + err: SerializedError, + duration: number + knownFailing: boolean + logs: string[] + testFile: string +} | { + type: "worker-finished", + forcedExit: boolean, + testFile: string +} | { + type: "worker-failed", + nonZeroExitCode?: boolean, + signal?: string, + err?: SerializedError +} | { + type: "touched-files", + files: { + changedFiles: string[], + temporaryFiles: string[] + } +} | { + type: 'worker-stdout', + chunk: Uint8Array + testFile: string +} | { + type: 'worker-stderr', + chunk: Uint8Array + testFile: string +} | { + type: "timeout", + period: number, + pendingTests: Map> +} + | { + type: "end" +} From 18f0cd33fe580378a798f2b222038e85dc2c7c0a Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 25 Sep 2023 15:06:25 -0700 Subject: [PATCH 08/16] Remove unnecssary async changes --- lib/cli.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index e8d61fbd1..cb22f9446 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -478,10 +478,10 @@ export default async function loadCli() { // eslint-disable-line complexity }); } - api.on('run', async plan => { + api.on('run', plan => { reporter.startRun(plan); - plan.status.on('stateChange', async evt => { + plan.status.on('stateChange', evt => { if (evt.type === 'end' || evt.type === 'interrupt') { // Write out code coverage data when the run ends, lest a process // interrupt causes it to be lost. From b7c80536d32ea4aea77c674d27ed8c46d7f9b63c Mon Sep 17 00:00:00 2001 From: Max Isom Date: Fri, 27 Oct 2023 16:29:21 -0700 Subject: [PATCH 09/16] Rename Run -> ObservedRun --- entrypoints/internal.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoints/internal.d.ts b/entrypoints/internal.d.ts index b3392c4a3..13c8faa69 100644 --- a/entrypoints/internal.d.ts +++ b/entrypoints/internal.d.ts @@ -20,6 +20,6 @@ export type RunEvent = { export type {StateChangeEvent} from '../types/state-change-events.d.cts'; -export type Run = { +export type ObservedRun = { events: AsyncIterableIterator; }; From 1d4de13885cc8e8ad97a9ab79fcad75a380116e5 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Fri, 27 Oct 2023 16:48:57 -0700 Subject: [PATCH 10/16] Clean up exports, only export types/ESM --- entrypoints/internal.cjs | 1 - entrypoints/internal.d.cts | 1 - entrypoints/internal.d.mts | 26 +++++++++++++++++++++++++- entrypoints/internal.d.ts | 25 ------------------------- entrypoints/internal.mjs | 1 - internal.d.ts | 3 --- package.json | 9 +-------- 7 files changed, 26 insertions(+), 40 deletions(-) delete mode 100644 entrypoints/internal.cjs delete mode 100644 entrypoints/internal.d.cts delete mode 100644 entrypoints/internal.d.ts delete mode 100644 entrypoints/internal.mjs delete mode 100644 internal.d.ts diff --git a/entrypoints/internal.cjs b/entrypoints/internal.cjs deleted file mode 100644 index f053ebf79..000000000 --- a/entrypoints/internal.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/entrypoints/internal.d.cts b/entrypoints/internal.d.cts deleted file mode 100644 index 09f74b9a2..000000000 --- a/entrypoints/internal.d.cts +++ /dev/null @@ -1 +0,0 @@ -export * from "./internal" diff --git a/entrypoints/internal.d.mts b/entrypoints/internal.d.mts index 09f74b9a2..13c8faa69 100644 --- a/entrypoints/internal.d.mts +++ b/entrypoints/internal.d.mts @@ -1 +1,25 @@ -export * from "./internal" +import type {StateChangeEvent} from '../types/state-change-events.d.cts'; + +export type RunEvent = { + type: 'stateChange'; + stateChange: StateChangeEvent; +} | { + type: 'run'; + plan: { + bailWithoutReporting: boolean; + debug: boolean; + failFastEnabled: boolean; + filePathPrefix: string; + files: string[]; + matching: boolean; + previousFailures: number; + runOnlyExclusive: boolean; + firstRun: boolean; + }; +}; + +export type {StateChangeEvent} from '../types/state-change-events.d.cts'; + +export type ObservedRun = { + events: AsyncIterableIterator; +}; diff --git a/entrypoints/internal.d.ts b/entrypoints/internal.d.ts deleted file mode 100644 index 13c8faa69..000000000 --- a/entrypoints/internal.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type {StateChangeEvent} from '../types/state-change-events.d.cts'; - -export type RunEvent = { - type: 'stateChange'; - stateChange: StateChangeEvent; -} | { - type: 'run'; - plan: { - bailWithoutReporting: boolean; - debug: boolean; - failFastEnabled: boolean; - filePathPrefix: string; - files: string[]; - matching: boolean; - previousFailures: number; - runOnlyExclusive: boolean; - firstRun: boolean; - }; -}; - -export type {StateChangeEvent} from '../types/state-change-events.d.cts'; - -export type ObservedRun = { - events: AsyncIterableIterator; -}; diff --git a/entrypoints/internal.mjs b/entrypoints/internal.mjs deleted file mode 100644 index cb0ff5c3b..000000000 --- a/entrypoints/internal.mjs +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/internal.d.ts b/internal.d.ts deleted file mode 100644 index 7971cabce..000000000 --- a/internal.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// For compatibility with resolution algorithms other than Node16. - -export * from './entrypoints/internal.cjs'; diff --git a/package.json b/package.json index 88c04fdce..dbabf7a04 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,7 @@ } }, "./internal": { - "import": { - "types": "./entrypoints/internal.d.mts", - "default": "./entrypoints/internal.mjs" - }, - "require": { - "types": "./entrypoints/internal.d.cts", - "default": "./entrypoints/internal.cjs" - } + "types": "./entrypoints/internal.d.mts" } }, "type": "module", From e15de6950f066f28b068c57c0c38d44a6907b49f Mon Sep 17 00:00:00 2001 From: Max Isom Date: Fri, 27 Oct 2023 17:10:52 -0700 Subject: [PATCH 11/16] End iterator when run is finished --- lib/api-event-iterator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/api-event-iterator.js b/lib/api-event-iterator.js index 3ceb252fb..9f325cdfe 100644 --- a/lib/api-event-iterator.js +++ b/lib/api-event-iterator.js @@ -12,6 +12,10 @@ export async function * asyncEventIteratorFromApi(api) { type: 'stateChange', stateChange, }; + + if (stateChange.type === 'end') { + break; + } } } } From f10831b5a96cf0bb391ca58eca460b253d0509fb Mon Sep 17 00:00:00 2001 From: Max Isom Date: Fri, 27 Oct 2023 17:28:50 -0700 Subject: [PATCH 12/16] State change -> event --- entrypoints/internal.d.mts | 22 ++----------------- lib/api-event-iterator.js | 13 +---------- test/internal-events/fixtures/ava.config.js | 2 +- test/internal-events/test.js | 24 ++++++--------------- 4 files changed, 11 insertions(+), 50 deletions(-) diff --git a/entrypoints/internal.d.mts b/entrypoints/internal.d.mts index 13c8faa69..8afc573ea 100644 --- a/entrypoints/internal.d.mts +++ b/entrypoints/internal.d.mts @@ -1,25 +1,7 @@ import type {StateChangeEvent} from '../types/state-change-events.d.cts'; -export type RunEvent = { - type: 'stateChange'; - stateChange: StateChangeEvent; -} | { - type: 'run'; - plan: { - bailWithoutReporting: boolean; - debug: boolean; - failFastEnabled: boolean; - filePathPrefix: string; - files: string[]; - matching: boolean; - previousFailures: number; - runOnlyExclusive: boolean; - firstRun: boolean; - }; -}; - -export type {StateChangeEvent} from '../types/state-change-events.d.cts'; +export type Event = StateChangeEvent; export type ObservedRun = { - events: AsyncIterableIterator; + events: AsyncIterableIterator; }; diff --git a/lib/api-event-iterator.js b/lib/api-event-iterator.js index 9f325cdfe..4012d6948 100644 --- a/lib/api-event-iterator.js +++ b/lib/api-event-iterator.js @@ -2,20 +2,9 @@ import {on} from 'node:events'; export async function * asyncEventIteratorFromApi(api) { for await (const [plan] of on(api, 'run')) { - yield { - type: 'run', - plan, - }; for await (const [stateChange] of on(plan.status, 'stateChange')) { - yield { - type: 'stateChange', - stateChange, - }; - - if (stateChange.type === 'end') { - break; - } + yield stateChange; } } } diff --git a/test/internal-events/fixtures/ava.config.js b/test/internal-events/fixtures/ava.config.js index 62066753a..0fcf9b09e 100644 --- a/test/internal-events/fixtures/ava.config.js +++ b/test/internal-events/fixtures/ava.config.js @@ -13,7 +13,7 @@ export default { for await (const event of run.events) { internalEvents.push(event); - if (event.type === 'stateChange' && event.stateChange.type === 'end') { + if (event.type === 'end') { await fs.writeFile('internal-events.json', JSON.stringify(internalEvents)); } } diff --git a/test/internal-events/test.js b/test/internal-events/test.js index 60cd73441..b1cd186f0 100644 --- a/test/internal-events/test.js +++ b/test/internal-events/test.js @@ -11,28 +11,18 @@ test('internal events are emitted', async t => { const result = JSON.parse(await fs.readFile(fileURLToPath(new URL('fixtures/internal-events.json', import.meta.url)))); t.like(result[0], { - type: 'run', - plan: { - files: [ - fileURLToPath(new URL('fixtures/test.js', import.meta.url)), - ], - }, + type: 'starting', + testFile: fileURLToPath(new URL('fixtures/test.js', import.meta.url)), }); - const testPassedEvent = result.find(event => event.type === 'stateChange' && event.stateChange.type === 'test-passed'); + const testPassedEvent = result.find(event => event.type === 'test-passed'); t.like(testPassedEvent, { - type: 'stateChange', - stateChange: { - type: 'test-passed', - title: 'placeholder', - testFile: fileURLToPath(new URL('fixtures/test.js', import.meta.url)), - }, + type: 'test-passed', + title: 'placeholder', + testFile: fileURLToPath(new URL('fixtures/test.js', import.meta.url)), }); t.like(result[result.length - 1], { - type: 'stateChange', - stateChange: { - type: 'end', - }, + type: 'end', }); }); From fdca2edc47bda58bc6aa7f8ce7eac85c6225aa3f Mon Sep 17 00:00:00 2001 From: Max Isom Date: Fri, 27 Oct 2023 17:57:36 -0700 Subject: [PATCH 13/16] Cause error in test --- test/internal-events/fixtures/ava.config.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/internal-events/fixtures/ava.config.js b/test/internal-events/fixtures/ava.config.js index 0fcf9b09e..034b7d501 100644 --- a/test/internal-events/fixtures/ava.config.js +++ b/test/internal-events/fixtures/ava.config.js @@ -12,10 +12,8 @@ export default { async observeRun(run) { for await (const event of run.events) { internalEvents.push(event); - - if (event.type === 'end') { - await fs.writeFile('internal-events.json', JSON.stringify(internalEvents)); - } } + + await fs.writeFile('internal-events.json', JSON.stringify(internalEvents)); }, }; From a9eac471bbbcb2a645c4de2da9fe96e123ecfc27 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Thu, 9 Nov 2023 12:04:44 -0800 Subject: [PATCH 14/16] Fix test --- lib/api-event-iterator.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/api-event-iterator.js b/lib/api-event-iterator.js index 4012d6948..1b2b55bf1 100644 --- a/lib/api-event-iterator.js +++ b/lib/api-event-iterator.js @@ -1,10 +1,12 @@ -import {on} from 'node:events'; - export async function * asyncEventIteratorFromApi(api) { - for await (const [plan] of on(api, 'run')) { + // TODO: support multiple runs (watch mode) + const {value: plan} = await api.events('run').next(); + + for await (const stateChange of plan.status.events('stateChange')) { + yield stateChange; - for await (const [stateChange] of on(plan.status, 'stateChange')) { - yield stateChange; + if (stateChange.type === 'end' || stateChange.type === 'interrupt') { + break; } } } From ecef15a74b37b6f3865a8ff5b531a52499e9f2bb Mon Sep 17 00:00:00 2001 From: Max Isom Date: Thu, 9 Nov 2023 12:13:20 -0800 Subject: [PATCH 15/16] Fix lint issues --- entrypoints/internal.d.mts | 2 +- test/internal-events/test.js | 2 +- types/state-change-events.d.cts | 237 ++++++++++++++++---------------- 3 files changed, 121 insertions(+), 120 deletions(-) diff --git a/entrypoints/internal.d.mts b/entrypoints/internal.d.mts index 8afc573ea..753b780e5 100644 --- a/entrypoints/internal.d.mts +++ b/entrypoints/internal.d.mts @@ -1,4 +1,4 @@ -import type {StateChangeEvent} from '../types/state-change-events.d.cts'; +import type {StateChangeEvent} from '../types/state-change-events.d'; export type Event = StateChangeEvent; diff --git a/test/internal-events/test.js b/test/internal-events/test.js index b1cd186f0..dbfb6e56c 100644 --- a/test/internal-events/test.js +++ b/test/internal-events/test.js @@ -22,7 +22,7 @@ test('internal events are emitted', async t => { testFile: fileURLToPath(new URL('fixtures/test.js', import.meta.url)), }); - t.like(result[result.length - 1], { + t.like(result.at(-1), { type: 'end', }); }); diff --git a/types/state-change-events.d.cts b/types/state-change-events.d.cts index fd481b2fa..9368a614f 100644 --- a/types/state-change-events.d.cts +++ b/types/state-change-events.d.cts @@ -1,143 +1,144 @@ type ErrorSource = { - isDependency: boolean - isWithinProject: boolean - file: string - line: number -} + isDependency: boolean; + isWithinProject: boolean; + file: string; + line: number; +}; type SerializedErrorBase = { - message: string - name: string, - originalError: unknown, - stack: string -} + message: string; + name: string; + originalError: unknown; + stack: string; +}; type AggregateSerializedError = SerializedErrorBase & { - type: "aggregate" - errors: SerializedError[] -} + type: 'aggregate'; + errors: SerializedError[]; +}; type NativeSerializedError = SerializedErrorBase & { - type: "native" - source: ErrorSource | null -} + type: 'native'; + source: ErrorSource | undefined; +}; +// eslint-disable-next-line @typescript-eslint/naming-convention type AVASerializedError = SerializedErrorBase & { - type: "ava" - assertion: string - improperUsage: unknown | null - formattedCause: unknown | null - formattedDetails: unknown | unknown[] - source: ErrorSource | null -} + type: 'ava'; + assertion: string; + improperUsage: unknown | undefined; + formattedCause: unknown | undefined; + formattedDetails: unknown | unknown[]; + source: ErrorSource | undefined; +}; -type SerializedError = AggregateSerializedError | NativeSerializedError | AVASerializedError +type SerializedError = AggregateSerializedError | NativeSerializedError | AVASerializedError; export type StateChangeEvent = { - type: "starting", - testFile: string + type: 'starting'; + testFile: string; } | { - type: "stats", - stats: { - byFile: Map - declaredTests: number - failedHooks: number, - failedTests: number, - failedWorkers: number, - files: number, - parallelRuns: { - currentIndex: number, - totalRuns: number - } | null - finishedWorkers: number, - internalErrors: number - remainingTests: number, - passedKnownFailingTests: number, - passedTests: number, - selectedTests: number, - sharedWorkerErrors: number, - skippedTests: number, - timedOutTests: number, - timeouts: number, - todoTests: number, - uncaughtExceptions: number, - unhandledRejections: number, - } + type: 'stats'; + stats: { + byFile: Map; + declaredTests: number; + failedHooks: number; + failedTests: number; + failedWorkers: number; + files: number; + parallelRuns: { + currentIndex: number; + totalRuns: number; + } | undefined; + finishedWorkers: number; + internalErrors: number; + remainingTests: number; + passedKnownFailingTests: number; + passedTests: number; + selectedTests: number; + sharedWorkerErrors: number; + skippedTests: number; + timedOutTests: number; + timeouts: number; + todoTests: number; + uncaughtExceptions: number; + unhandledRejections: number; + }; } | { - type: "declared-test" - title: string - knownFailing: boolean - todo: boolean - testFile: string + type: 'declared-test'; + title: string; + knownFailing: boolean; + todo: boolean; + testFile: string; } | { - type: "selected-test" - title: string - knownFailing: boolean - skip: boolean - todo: boolean - testFile: string + type: 'selected-test'; + title: string; + knownFailing: boolean; + skip: boolean; + todo: boolean; + testFile: string; } | { - type: "test-register-log-reference" - title: string - logs: string[] - testFile: string + type: 'test-register-log-reference'; + title: string; + logs: string[]; + testFile: string; } | { - type: "test-passed", - title: string - duration: number - knownFailing: boolean - logs: string[] - testFile: string + type: 'test-passed'; + title: string; + duration: number; + knownFailing: boolean; + logs: string[]; + testFile: string; } | { - type: "test-failed", - title: string - err: SerializedError, - duration: number - knownFailing: boolean - logs: string[] - testFile: string + type: 'test-failed'; + title: string; + err: SerializedError; + duration: number; + knownFailing: boolean; + logs: string[]; + testFile: string; } | { - type: "worker-finished", - forcedExit: boolean, - testFile: string + type: 'worker-finished'; + forcedExit: boolean; + testFile: string; } | { - type: "worker-failed", - nonZeroExitCode?: boolean, - signal?: string, - err?: SerializedError + type: 'worker-failed'; + nonZeroExitCode?: boolean; + signal?: string; + err?: SerializedError; } | { - type: "touched-files", - files: { - changedFiles: string[], - temporaryFiles: string[] - } + type: 'touched-files'; + files: { + changedFiles: string[]; + temporaryFiles: string[]; + }; } | { - type: 'worker-stdout', - chunk: Uint8Array - testFile: string + type: 'worker-stdout'; + chunk: Uint8Array; + testFile: string; } | { - type: 'worker-stderr', - chunk: Uint8Array - testFile: string + type: 'worker-stderr'; + chunk: Uint8Array; + testFile: string; } | { - type: "timeout", - period: number, - pendingTests: Map> -} - | { - type: "end" + type: 'timeout'; + period: number; + pendingTests: Map>; } +| { + type: 'end'; +}; From 11d9049ce7b82e08a1e166987b641af812200b60 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 26 Nov 2023 21:12:43 +0100 Subject: [PATCH 16/16] Rename type --- types/state-change-events.d.cts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/types/state-change-events.d.cts b/types/state-change-events.d.cts index 9368a614f..fe37f517c 100644 --- a/types/state-change-events.d.cts +++ b/types/state-change-events.d.cts @@ -22,8 +22,7 @@ type NativeSerializedError = SerializedErrorBase & { source: ErrorSource | undefined; }; -// eslint-disable-next-line @typescript-eslint/naming-convention -type AVASerializedError = SerializedErrorBase & { +type AvaSerializedError = SerializedErrorBase & { type: 'ava'; assertion: string; improperUsage: unknown | undefined; @@ -32,7 +31,7 @@ type AVASerializedError = SerializedErrorBase & { source: ErrorSource | undefined; }; -type SerializedError = AggregateSerializedError | NativeSerializedError | AVASerializedError; +type SerializedError = AggregateSerializedError | NativeSerializedError | AvaSerializedError; export type StateChangeEvent = { type: 'starting';