Skip to content

Commit adbfcde

Browse files
authored
Experimentally expose internal events for custom reporters
Add a new `observeRunsFromConfig` experiment, which allows a test run to be observed by a function installed through an `ava.config.*` file. The function has access to AVA's internal events, which can then be used to report to a file. AVA's internal event structure is not currently covered by any SemVer guarantees, which is why this feature requires the experimental opt-in. Does not currently support watch mode. Only the first run is observed.
1 parent 6790d50 commit adbfcde

11 files changed

+229
-1
lines changed

entrypoints/internal.d.mts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type {StateChangeEvent} from '../types/state-change-events.d';
2+
3+
export type Event = StateChangeEvent;
4+
5+
export type ObservedRun = {
6+
events: AsyncIterableIterator<Event>;
7+
};

lib/api-event-iterator.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export async function * asyncEventIteratorFromApi(api) {
2+
// TODO: support multiple runs (watch mode)
3+
const {value: plan} = await api.events('run').next();
4+
5+
for await (const stateChange of plan.status.events('stateChange')) {
6+
yield stateChange;
7+
8+
if (stateChange.type === 'end' || stateChange.type === 'interrupt') {
9+
break;
10+
}
11+
}
12+
}

lib/cli.js

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import figures from 'figures';
88
import yargs from 'yargs';
99
import {hideBin} from 'yargs/helpers'; // eslint-disable-line n/file-extension-in-import
1010

11+
import {asyncEventIteratorFromApi} from './api-event-iterator.js';
1112
import Api from './api.js';
1213
import {chalk} from './chalk.js';
1314
import validateEnvironmentVariables from './environment-variables.js';
@@ -470,6 +471,12 @@ export default async function loadCli() { // eslint-disable-line complexity
470471
});
471472
}
472473

474+
if (combined.observeRun && experiments.observeRunsFromConfig) {
475+
combined.observeRun({
476+
events: asyncEventIteratorFromApi(api),
477+
});
478+
}
479+
473480
api.on('run', plan => {
474481
reporter.startRun(plan);
475482

lib/load-config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {packageConfig, packageJsonPath} from 'package-config';
88

99
const NO_SUCH_FILE = Symbol('no ava.config.js file');
1010
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
11-
const EXPERIMENTS = new Set();
11+
const EXPERIMENTS = new Set(['observeRunsFromConfig']);
1212

1313
const importConfig = async ({configFile, fileForErrorMessage}) => {
1414
const {default: config = MISSING_DEFAULT_EXPORT} = await import(url.pathToFileURL(configFile));

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
"types": "./entrypoints/plugin.d.cts",
3030
"default": "./entrypoints/plugin.cjs"
3131
}
32+
},
33+
"./internal": {
34+
"types": "./entrypoints/internal.d.mts"
3235
}
3336
},
3437
"type": "module",
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
internal-events.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import fs from 'node:fs/promises';
2+
3+
const internalEvents = [];
4+
5+
export default {
6+
files: [
7+
'test.js',
8+
],
9+
nonSemVerExperiments: {
10+
observeRunsFromConfig: true,
11+
},
12+
async observeRun(run) {
13+
for await (const event of run.events) {
14+
internalEvents.push(event);
15+
}
16+
17+
await fs.writeFile('internal-events.json', JSON.stringify(internalEvents));
18+
},
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "module"
3+
}

test/internal-events/fixtures/test.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from 'ava';
2+
3+
test('placeholder', t => {
4+
t.pass();
5+
});

test/internal-events/test.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import fs from 'node:fs/promises';
2+
import {fileURLToPath} from 'node:url';
3+
4+
import test from '@ava/test';
5+
6+
import {fixture} from '../helpers/exec.js';
7+
8+
test('internal events are emitted', async t => {
9+
await fixture();
10+
11+
const result = JSON.parse(await fs.readFile(fileURLToPath(new URL('fixtures/internal-events.json', import.meta.url))));
12+
13+
t.like(result[0], {
14+
type: 'starting',
15+
testFile: fileURLToPath(new URL('fixtures/test.js', import.meta.url)),
16+
});
17+
18+
const testPassedEvent = result.find(event => event.type === 'test-passed');
19+
t.like(testPassedEvent, {
20+
type: 'test-passed',
21+
title: 'placeholder',
22+
testFile: fileURLToPath(new URL('fixtures/test.js', import.meta.url)),
23+
});
24+
25+
t.like(result.at(-1), {
26+
type: 'end',
27+
});
28+
});

types/state-change-events.d.cts

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
type ErrorSource = {
2+
isDependency: boolean;
3+
isWithinProject: boolean;
4+
file: string;
5+
line: number;
6+
};
7+
8+
type SerializedErrorBase = {
9+
message: string;
10+
name: string;
11+
originalError: unknown;
12+
stack: string;
13+
};
14+
15+
type AggregateSerializedError = SerializedErrorBase & {
16+
type: 'aggregate';
17+
errors: SerializedError[];
18+
};
19+
20+
type NativeSerializedError = SerializedErrorBase & {
21+
type: 'native';
22+
source: ErrorSource | undefined;
23+
};
24+
25+
type AvaSerializedError = SerializedErrorBase & {
26+
type: 'ava';
27+
assertion: string;
28+
improperUsage: unknown | undefined;
29+
formattedCause: unknown | undefined;
30+
formattedDetails: unknown | unknown[];
31+
source: ErrorSource | undefined;
32+
};
33+
34+
type SerializedError = AggregateSerializedError | NativeSerializedError | AvaSerializedError;
35+
36+
export type StateChangeEvent = {
37+
type: 'starting';
38+
testFile: string;
39+
} | {
40+
type: 'stats';
41+
stats: {
42+
byFile: Map<string, {
43+
declaredTests: number;
44+
failedHooks: number;
45+
failedTests: number;
46+
internalErrors: number;
47+
remainingTests: number;
48+
passedKnownFailingTests: number;
49+
passedTests: number;
50+
selectedTests: number;
51+
selectingLines: boolean;
52+
skippedTests: number;
53+
todoTests: number;
54+
uncaughtExceptions: number;
55+
unhandledRejections: number;
56+
}>;
57+
declaredTests: number;
58+
failedHooks: number;
59+
failedTests: number;
60+
failedWorkers: number;
61+
files: number;
62+
parallelRuns: {
63+
currentIndex: number;
64+
totalRuns: number;
65+
} | undefined;
66+
finishedWorkers: number;
67+
internalErrors: number;
68+
remainingTests: number;
69+
passedKnownFailingTests: number;
70+
passedTests: number;
71+
selectedTests: number;
72+
sharedWorkerErrors: number;
73+
skippedTests: number;
74+
timedOutTests: number;
75+
timeouts: number;
76+
todoTests: number;
77+
uncaughtExceptions: number;
78+
unhandledRejections: number;
79+
};
80+
} | {
81+
type: 'declared-test';
82+
title: string;
83+
knownFailing: boolean;
84+
todo: boolean;
85+
testFile: string;
86+
} | {
87+
type: 'selected-test';
88+
title: string;
89+
knownFailing: boolean;
90+
skip: boolean;
91+
todo: boolean;
92+
testFile: string;
93+
} | {
94+
type: 'test-register-log-reference';
95+
title: string;
96+
logs: string[];
97+
testFile: string;
98+
} | {
99+
type: 'test-passed';
100+
title: string;
101+
duration: number;
102+
knownFailing: boolean;
103+
logs: string[];
104+
testFile: string;
105+
} | {
106+
type: 'test-failed';
107+
title: string;
108+
err: SerializedError;
109+
duration: number;
110+
knownFailing: boolean;
111+
logs: string[];
112+
testFile: string;
113+
} | {
114+
type: 'worker-finished';
115+
forcedExit: boolean;
116+
testFile: string;
117+
} | {
118+
type: 'worker-failed';
119+
nonZeroExitCode?: boolean;
120+
signal?: string;
121+
err?: SerializedError;
122+
} | {
123+
type: 'touched-files';
124+
files: {
125+
changedFiles: string[];
126+
temporaryFiles: string[];
127+
};
128+
} | {
129+
type: 'worker-stdout';
130+
chunk: Uint8Array;
131+
testFile: string;
132+
} | {
133+
type: 'worker-stderr';
134+
chunk: Uint8Array;
135+
testFile: string;
136+
} | {
137+
type: 'timeout';
138+
period: number;
139+
pendingTests: Map<string, Set<string>>;
140+
}
141+
| {
142+
type: 'end';
143+
};

0 commit comments

Comments
 (0)