@feasibleone/blong-gogo 1.35.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/package.json +4 -3
- package/src/AdapterBase.ts +33 -5
- package/src/Gateway.ts +14 -16
- package/src/Registry.ts +15 -2
- package/src/RpcClient.ts +3 -3
- package/src/RpcServer.ts +2 -2
- package/src/SemanticLog.test.ts +67 -8
- package/src/SemanticLog.ts +1 -1
- package/src/Watch.ts +33 -4
- package/src/adapterProgress.test.ts +190 -0
- package/src/browserEagerImports.test.ts +62 -0
- package/src/chain.allure.test.ts +216 -0
- package/src/chain.ts +238 -2
- package/src/checkpoint.test.ts +265 -0
- package/src/checkpoint.ts +115 -14
- package/src/ci.intent.test.ts +94 -0
- package/src/codec/adapter/jsonrpc/receive.ts +4 -4
- package/src/codec/adapter/mle/ready.ts +39 -11
- package/src/load.ts +133 -20
- package/src/mle.ts +3 -3
- package/src/orchestrator/common/dispatch.ts +6 -5
- package/src/pino-cacache.ts +59 -3
- package/src/pino-pretty.ts +1 -1
- package/src/registry.lib.test.ts +97 -0
- package/src/semanticContext.test.ts +125 -35
- package/src/semanticContext.ts +51 -22
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {readFileSync, readdirSync} from 'node:fs';
|
|
2
|
+
import {dirname, join} from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {test} from 'tap';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Node-only modules must not be imported for a value.
|
|
8
|
+
//
|
|
9
|
+
// A browser build stubs what it cannot resolve and carries on, so an eager import of a
|
|
10
|
+
// node-only module survives CI and only shows up in a teardown-free dev server: vite
|
|
11
|
+
// links the module graph before any code runs and refuses a *named* import from its
|
|
12
|
+
// browser-external stub, which takes the whole page down ("The requested module
|
|
13
|
+
// '/@id/__vite-browser-external:node:events' does not provide an export named
|
|
14
|
+
// 'EventEmitter'"). The browser entry reaches `Watch.ts` through a dynamic import the
|
|
15
|
+
// loader executes at startup, so a static import inside it is as eager as one made by
|
|
16
|
+
// the entry itself — this scans the sources rather than a bundle because a dynamic edge
|
|
17
|
+
// hides the module from a bundler's static view.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
interface INodeOnlyModule {
|
|
23
|
+
/** Import specifier as written in the sources. */
|
|
24
|
+
specifier: string;
|
|
25
|
+
/** What makes it unusable in a browser. */
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NODE_ONLY: INodeOnlyModule[] = [
|
|
30
|
+
{
|
|
31
|
+
specifier: './chain.ts',
|
|
32
|
+
reason: 'it needs node:assert, node:test and the event emitter its executor extends',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** `import x from '...'`, `export {x} from '...'` and side-effect imports, but not `import type`. */
|
|
37
|
+
const eagerImport = (specifier: string) =>
|
|
38
|
+
new RegExp(
|
|
39
|
+
`^\\s*(?:import|export)\\s+(?!type\\s)[^;]*?from\\s*['"]${specifier}['"]|^\\s*import\\s*['"]${specifier}['"]`,
|
|
40
|
+
'gm',
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
test('a node-only module is never imported for a value', async t => {
|
|
44
|
+
const files = readdirSync(srcDir).filter(
|
|
45
|
+
name => name.endsWith('.ts') && !name.endsWith('.test.ts'),
|
|
46
|
+
);
|
|
47
|
+
t.ok(files.length > 0, 'the sources were found');
|
|
48
|
+
|
|
49
|
+
for (const {specifier, reason} of NODE_ONLY) {
|
|
50
|
+
const pattern = eagerImport(specifier);
|
|
51
|
+
const offenders = files.flatMap(name =>
|
|
52
|
+
[...readFileSync(join(srcDir, name), 'utf8').matchAll(pattern)].map(
|
|
53
|
+
match => `${name}: ${match[0].trim()}`,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
t.same(
|
|
57
|
+
offenders,
|
|
58
|
+
[],
|
|
59
|
+
`${specifier} is imported eagerly (${reason}); import it dynamically, or for its types only:\n ${offenders.join('\n ')}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the chain runner does with Allure reporting when it cannot have it.
|
|
3
|
+
*
|
|
4
|
+
* Allure 3's format is a directory of files, so the integration needs a filesystem.
|
|
5
|
+
* A browser platform has none, and the lazy import of `blong-allure` there can only
|
|
6
|
+
* fail — which the runner used to discover once per group, leaving a run with no
|
|
7
|
+
* results and no explanation of why. These tests pin the two halves of the fix: the
|
|
8
|
+
* platform decides *before* the attempt, and the reason is reported once for the run
|
|
9
|
+
* rather than once per group.
|
|
10
|
+
*
|
|
11
|
+
* Each test imports the module fresh: the session, the run's options and the reason
|
|
12
|
+
* it cannot report are module-level state, and a test that inherited them from the
|
|
13
|
+
* one before it would be asserting the previous test's outcome.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {existsSync} from 'node:fs';
|
|
17
|
+
import {mkdtemp, readdir, readFile, rm} from 'node:fs/promises';
|
|
18
|
+
import {tmpdir} from 'node:os';
|
|
19
|
+
import {join} from 'node:path';
|
|
20
|
+
import {test} from 'tap';
|
|
21
|
+
|
|
22
|
+
/** A test context that runs what it is given, the way the runner's own does. */
|
|
23
|
+
interface IContext {
|
|
24
|
+
test: (name: string, fn: (t: IContext) => unknown) => Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function contextAt(): IContext {
|
|
28
|
+
return {
|
|
29
|
+
test: async (_name: string, fn: (t: IContext) => unknown) => fn(contextAt()),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A group's steps, named the way the runner names them from the wire name. */
|
|
34
|
+
function group(name: string, steps: Array<() => unknown>): Array<() => unknown> {
|
|
35
|
+
const wrapped = [...steps];
|
|
36
|
+
Object.defineProperty(wrapped, 'name', {value: name});
|
|
37
|
+
return wrapped;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The log the runner is handed, recording what a run would have shown a reader. */
|
|
41
|
+
function recordingLog(): {
|
|
42
|
+
warnings: string[];
|
|
43
|
+
errors: unknown[];
|
|
44
|
+
log: {warn: (text: unknown) => void; error: (error: unknown) => void};
|
|
45
|
+
} {
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
const errors: unknown[] = [];
|
|
48
|
+
return {
|
|
49
|
+
warnings,
|
|
50
|
+
errors,
|
|
51
|
+
log: {
|
|
52
|
+
warn: (text: unknown) => void warnings.push(String(text)),
|
|
53
|
+
error: (error: unknown) => void errors.push(error),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('a browser run is told why it produces no report, once for the run', async t => {
|
|
59
|
+
const dir = await mkdtemp(join(tmpdir(), 'blong-chain-allure-'));
|
|
60
|
+
t.teardown(() => rm(dir, {recursive: true, force: true}));
|
|
61
|
+
const outputDir = join(dir, 'allure-results');
|
|
62
|
+
const {warnings, errors, log} = recordingLog();
|
|
63
|
+
const ran: string[] = [];
|
|
64
|
+
const {default: chainFactory} = await t.mockImport<typeof import('./chain.ts')>(
|
|
65
|
+
'./chain.ts',
|
|
66
|
+
{},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const chain = await chainFactory(contextAt() as never, log, {
|
|
70
|
+
method: 'test.order.checkpoint',
|
|
71
|
+
allure: {enabled: true, outputDir},
|
|
72
|
+
platform: 'browser',
|
|
73
|
+
});
|
|
74
|
+
const run = chain as unknown as (steps: unknown) => Promise<unknown>;
|
|
75
|
+
await run(
|
|
76
|
+
group('order checkpoint', [
|
|
77
|
+
async function loadsOrder() {
|
|
78
|
+
ran.push('loadsOrder');
|
|
79
|
+
return 'loaded';
|
|
80
|
+
},
|
|
81
|
+
]),
|
|
82
|
+
);
|
|
83
|
+
// A second group, whose own attempt would be a second opportunity to say the
|
|
84
|
+
// same thing: the answer belongs to the run, so it is said once.
|
|
85
|
+
await run(
|
|
86
|
+
group('payment settle', [
|
|
87
|
+
async function sendsPayment() {
|
|
88
|
+
ran.push('sendsPayment');
|
|
89
|
+
return 'sent';
|
|
90
|
+
},
|
|
91
|
+
]),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
t.same(
|
|
95
|
+
ran,
|
|
96
|
+
['loadsOrder', 'sendsPayment'],
|
|
97
|
+
'the groups ran: a withheld report is not a withheld test',
|
|
98
|
+
);
|
|
99
|
+
t.equal(warnings.length, 1, 'the reader is told once, not once per group');
|
|
100
|
+
t.match(warnings[0] ?? '', /browser platform cannot write its files/, 'and told the reason');
|
|
101
|
+
t.match(warnings[0] ?? '', /Allure reporting is enabled/, 'as a configuration mistake');
|
|
102
|
+
t.same(errors, [], 'nothing failed: the tests are judged on their tests');
|
|
103
|
+
t.notOk(existsSync(outputDir), 'and no results directory was created');
|
|
104
|
+
|
|
105
|
+
// The end of the run closes nothing, because nothing opened: no session means no
|
|
106
|
+
// import, which on this platform is the one thing that cannot work.
|
|
107
|
+
await (await import('./chain.ts')).endAllureSession();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('a server run opens the session and writes the group result', async t => {
|
|
111
|
+
const dir = await mkdtemp(join(tmpdir(), 'blong-chain-allure-'));
|
|
112
|
+
t.teardown(() => rm(dir, {recursive: true, force: true}));
|
|
113
|
+
const outputDir = join(dir, 'allure-results');
|
|
114
|
+
const {warnings, errors, log} = recordingLog();
|
|
115
|
+
const {default: chainFactory, endAllureSession} = await t.mockImport<
|
|
116
|
+
typeof import('./chain.ts')
|
|
117
|
+
>('./chain.ts', {});
|
|
118
|
+
|
|
119
|
+
const chain = await chainFactory(contextAt() as never, log, {
|
|
120
|
+
method: 'test.order.checkpoint',
|
|
121
|
+
allure: {enabled: true, outputDir},
|
|
122
|
+
platform: 'server',
|
|
123
|
+
});
|
|
124
|
+
await (chain as unknown as (steps: unknown) => Promise<unknown>)(
|
|
125
|
+
group('order checkpoint', [
|
|
126
|
+
async function loadsOrder() {
|
|
127
|
+
return 'loaded';
|
|
128
|
+
},
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
t.same(warnings, [], 'a server platform reports nothing and says nothing');
|
|
133
|
+
t.same(errors, []);
|
|
134
|
+
const written = await readdir(outputDir);
|
|
135
|
+
t.ok(
|
|
136
|
+
written.includes('environment.properties'),
|
|
137
|
+
'the session laid out its directory, as it does for a real run',
|
|
138
|
+
);
|
|
139
|
+
const resultFile = written.find(name => name.endsWith('-result.json'));
|
|
140
|
+
t.ok(resultFile, 'and the group left a result');
|
|
141
|
+
const result = JSON.parse(await readFile(join(outputDir, resultFile as string), 'utf8')) as {
|
|
142
|
+
name?: string;
|
|
143
|
+
labels?: Array<{name: string; value: string}>;
|
|
144
|
+
};
|
|
145
|
+
t.equal(result.name, 'order checkpoint', 'named as the group was run');
|
|
146
|
+
t.ok(
|
|
147
|
+
result.labels?.some(label => label.name === 'parentSuite' && label.value === 'test'),
|
|
148
|
+
'labelled from the group wire name',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// The end writes to stdout, which a tap run parses: its line is captured here
|
|
152
|
+
// rather than left in the middle of the suite's output.
|
|
153
|
+
const lines: string[] = [];
|
|
154
|
+
const original = console.log;
|
|
155
|
+
console.log = (...args: unknown[]) => void lines.push(args.join(' '));
|
|
156
|
+
try {
|
|
157
|
+
await endAllureSession();
|
|
158
|
+
} finally {
|
|
159
|
+
console.log = original;
|
|
160
|
+
}
|
|
161
|
+
t.match(
|
|
162
|
+
lines.join(''),
|
|
163
|
+
/Allure results written to/,
|
|
164
|
+
'the run ends by saying where the results are',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Nothing is left to close: the second end is a no-op rather than a second report.
|
|
168
|
+
await endAllureSession();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('a failure to start is reported once, and leaves nothing to close', async t => {
|
|
172
|
+
const dir = await mkdtemp(join(tmpdir(), 'blong-chain-allure-'));
|
|
173
|
+
t.teardown(() => rm(dir, {recursive: true, force: true}));
|
|
174
|
+
const {warnings, errors, log} = recordingLog();
|
|
175
|
+
let started = 0;
|
|
176
|
+
let ended = 0;
|
|
177
|
+
const {default: chainFactory, endAllureSession} = await t.mockImport<
|
|
178
|
+
typeof import('./chain.ts')
|
|
179
|
+
>('./chain.ts', {
|
|
180
|
+
// A server platform that cannot import the integration at all — a package
|
|
181
|
+
// missing on the runner, say. The runner cannot prevent this one; it can only
|
|
182
|
+
// say it once and carry on.
|
|
183
|
+
'@feasibleone/blong-allure': {
|
|
184
|
+
allureSessionStart: async () => {
|
|
185
|
+
started += 1;
|
|
186
|
+
throw new Error('blong-allure is not installed');
|
|
187
|
+
},
|
|
188
|
+
allureSessionEnd: async () => {
|
|
189
|
+
ended += 1;
|
|
190
|
+
},
|
|
191
|
+
allureGroupResultWrite: async () => undefined,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const chain = await chainFactory(contextAt() as never, log, {
|
|
196
|
+
method: 'test.order.checkpoint',
|
|
197
|
+
allure: {enabled: true, outputDir: join(dir, 'allure-results')},
|
|
198
|
+
platform: 'server',
|
|
199
|
+
});
|
|
200
|
+
const run = chain as unknown as (steps: unknown) => Promise<unknown>;
|
|
201
|
+
const step = async function loadsOrder() {
|
|
202
|
+
return 'loaded';
|
|
203
|
+
};
|
|
204
|
+
await run(group('order checkpoint', [step]));
|
|
205
|
+
await run(group('payment settle', [step]));
|
|
206
|
+
|
|
207
|
+
t.equal(started, 1, 'one attempt, however many groups ask for a report');
|
|
208
|
+
t.equal(errors.length, 1, 'and one report of it, not one per group');
|
|
209
|
+
t.match(String(errors[0]), /blong-allure is not installed/, 'naming what went wrong');
|
|
210
|
+
t.same(warnings, [], 'an unexpected failure is not a platform refusal');
|
|
211
|
+
|
|
212
|
+
// A session that never opened is nothing to close: the end is a no-op, so a run
|
|
213
|
+
// that could not write results does not also try to generate a report from them.
|
|
214
|
+
await endAllureSession();
|
|
215
|
+
t.equal(ended, 0, 'the end of the run closes nothing');
|
|
216
|
+
});
|
package/src/chain.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {TestExecutor, type ITestLogger} from '@feasibleone/blong-chain';
|
|
1
|
+
import {TestExecutor, type IStepProgress, type ITestLogger} from '@feasibleone/blong-chain';
|
|
2
2
|
import {withProgress} from '@feasibleone/blong-lib';
|
|
3
|
+
import type {IPlatformApi} from '@feasibleone/blong/types';
|
|
3
4
|
import assert from 'node:assert';
|
|
4
5
|
|
|
5
6
|
type Step = (a: typeof assert, results: object) => object | Promise<object>;
|
|
@@ -12,10 +13,227 @@ interface ITestContext {
|
|
|
12
13
|
test: (name: string, fn: (t: unknown) => void | Promise<void>) => unknown;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** Allure reporting options, as the framework config supplies them. */
|
|
17
|
+
export interface IAllureRunOptions {
|
|
18
|
+
/** Write results at all. Off by default, on for a captured (CI) run. */
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
/** Results directory (default `allure-results`). */
|
|
21
|
+
outputDir?: string;
|
|
22
|
+
/** Trend history file (default `.allure/history.jsonl`). */
|
|
23
|
+
historyPath?: string;
|
|
24
|
+
/** Generate the HTML report when the session ends. */
|
|
25
|
+
generateOnEnd?: boolean;
|
|
26
|
+
/** Pattern with `{traceId}`, used to link a result to its log trace. */
|
|
27
|
+
logUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IChainOptions {
|
|
31
|
+
/** The group's wire name, e.g. `test.order.checkpoint` — what Allure labels come from. */
|
|
32
|
+
method?: string;
|
|
33
|
+
allure?: IAllureRunOptions;
|
|
34
|
+
/**
|
|
35
|
+
* The platform this run belongs to, as `IPlatformApi` names it.
|
|
36
|
+
*
|
|
37
|
+
* Allure 3's format is a directory of files, so producing one needs a
|
|
38
|
+
* filesystem — and the browser platform has none. The platform is therefore what
|
|
39
|
+
* decides whether the attempt is made at all (see `canWriteReport`). A caller
|
|
40
|
+
* that names no platform is read as the server, which is what a direct chain run
|
|
41
|
+
* in Node is: refusing to report there would be worse than trying.
|
|
42
|
+
*/
|
|
43
|
+
platform?: IPlatformApi['platform'];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `@feasibleone/blong-allure`, assembled rather than written out.
|
|
48
|
+
*
|
|
49
|
+
* A specifier written as a literal is *resolved* by the bundler even when the import
|
|
50
|
+
* is dynamic, so the whole integration was landing in every browser build: its modules
|
|
51
|
+
* import `node:fs`, `node:fs/promises`, `node:crypto`, `node:url` and
|
|
52
|
+
* `node:child_process`, none of which a browser can provide, and
|
|
53
|
+
* `browserBundle.test.ts` failed on exactly that list. Building the string at runtime
|
|
54
|
+
* leaves the specifier opaque to the bundler, which is the same trick `load.ts` uses
|
|
55
|
+
* (`extension` there) to keep the server half of the platform map out of a browser
|
|
56
|
+
* build — and it costs nothing on the server, where Node resolves the specifier when
|
|
57
|
+
* it is imported.
|
|
58
|
+
*
|
|
59
|
+
* The import is never *made* on a browser either: `canWriteReport` refuses the
|
|
60
|
+
* platform before it, because a browser has nowhere to write a report.
|
|
61
|
+
*/
|
|
62
|
+
const allurePackage = (): string => ['@feasibleone', 'blong-allure'].join('/');
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The one Allure session of this process, started by the first group that runs.
|
|
66
|
+
*
|
|
67
|
+
* A session clears its results directory, so it cannot be per group: the first
|
|
68
|
+
* group would wipe the results of the ones before it. It is started lazily
|
|
69
|
+
* instead, and ended by {@link endAllureSession} once every group has finished.
|
|
70
|
+
*/
|
|
71
|
+
let session: Promise<void> | undefined;
|
|
72
|
+
|
|
73
|
+
/** The options the running session was started with, for its end. */
|
|
74
|
+
let sessionOptions: IAllureRunOptions | undefined;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Why this process will produce no Allure report, once that is known.
|
|
78
|
+
*
|
|
79
|
+
* Held for the run rather than per group, because the answer is the run's: a
|
|
80
|
+
* second group that says the same thing again adds noise to a run that has already
|
|
81
|
+
* explained why the report it asked for will not exist. `session` is never set
|
|
82
|
+
* while this is, so a blocked run also does not try to close a session later.
|
|
83
|
+
*/
|
|
84
|
+
let blocked: string | undefined;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether this platform can write Allure's files at all.
|
|
88
|
+
*
|
|
89
|
+
* The browser platform is the one that cannot: it has no `node:fs`, so the lazy
|
|
90
|
+
* import of `blong-allure` there can only fail — and a failure caught per group is
|
|
91
|
+
* a failure reported per group over a run whose tests were fine. Its tests report
|
|
92
|
+
* through their own producer (Allure's Playwright integration), so the results this
|
|
93
|
+
* guard withholds are ones that were never going to exist on that platform.
|
|
94
|
+
*
|
|
95
|
+
* The allowed kinds are listed rather than the refused one, so a platform added
|
|
96
|
+
* later has to be considered here instead of inheriting a report nobody can write.
|
|
97
|
+
*/
|
|
98
|
+
function canWriteReport(platform: IChainOptions['platform']): boolean {
|
|
99
|
+
return platform === undefined || platform === 'server';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The results directory and history file, defaulted the way Allure documents them. */
|
|
103
|
+
function sessionConfig(options: IAllureRunOptions): {
|
|
104
|
+
outputDir: string;
|
|
105
|
+
historyPath: string;
|
|
106
|
+
logUrl?: string;
|
|
107
|
+
} {
|
|
108
|
+
return {
|
|
109
|
+
outputDir: options.outputDir ?? 'allure-results',
|
|
110
|
+
historyPath: options.historyPath ?? '.allure/history.jsonl',
|
|
111
|
+
logUrl: options.logUrl,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Start the Allure session if this run wants one, once.
|
|
117
|
+
*
|
|
118
|
+
* `blong-allure` is imported here rather than at the top of the file, for the
|
|
119
|
+
* reason the log implementation is chosen lazily: a run that does not report to
|
|
120
|
+
* Allure should not load it.
|
|
121
|
+
*
|
|
122
|
+
* The platform is checked *before* the attempt, and the reason it cannot report is
|
|
123
|
+
* said out loud — as a configuration mistake rather than an error in a test, so the
|
|
124
|
+
* absence of a report is explained rather than discovered.
|
|
125
|
+
*
|
|
126
|
+
* An unexpected failure (a package that cannot be imported on a platform that
|
|
127
|
+
* should be able to write results) is reported on the same terms, and the session
|
|
128
|
+
* is dropped rather than left rejected: there is nothing for the end of the run to
|
|
129
|
+
* close, and a later group must not await a promise that has already failed.
|
|
130
|
+
*
|
|
131
|
+
* This never throws. A report that cannot be written is not a test that failed —
|
|
132
|
+
* the run is judged on its tests — and a caller that had to guard the call would be
|
|
133
|
+
* one place too many for that decision to live.
|
|
134
|
+
*/
|
|
135
|
+
async function ensureSession(
|
|
136
|
+
options: IAllureRunOptions | undefined,
|
|
137
|
+
platform: IChainOptions['platform'],
|
|
138
|
+
log?: ITestLogger,
|
|
139
|
+
): Promise<boolean> {
|
|
140
|
+
if (options?.enabled !== true || blocked !== undefined) return false;
|
|
141
|
+
if (!canWriteReport(platform)) {
|
|
142
|
+
blocked =
|
|
143
|
+
`Allure reporting is enabled for this run but the ${platform} platform cannot write its ` +
|
|
144
|
+
'files, so a report will not be produced';
|
|
145
|
+
log?.warn?.(blocked);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (session === undefined) {
|
|
149
|
+
sessionOptions = options;
|
|
150
|
+
session = (async () => {
|
|
151
|
+
const {allureSessionStart} = await import(/* @vite-ignore */ allurePackage());
|
|
152
|
+
await allureSessionStart(sessionConfig(options));
|
|
153
|
+
})();
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
await session;
|
|
157
|
+
return true;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// The first group to lose the session reports it; a group that was awaiting
|
|
160
|
+
// the same start loses it too and stays quiet, because the reason is the
|
|
161
|
+
// run's rather than its own.
|
|
162
|
+
session = undefined;
|
|
163
|
+
sessionOptions = undefined;
|
|
164
|
+
if (blocked === undefined) {
|
|
165
|
+
blocked = `Allure reporting failed: ${String(error)}`;
|
|
166
|
+
log?.error?.(error);
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The Allure context for a group's wire name.
|
|
174
|
+
*
|
|
175
|
+
* `test.order.checkpoint` reads as realm `test`, collection `order`, group
|
|
176
|
+
* `checkpoint`. A name that does not split stays the realm, so a suite with its
|
|
177
|
+
* own naming still gets a label rather than a result filed under nothing.
|
|
178
|
+
*/
|
|
179
|
+
function contextOf(method: string | undefined, options: IAllureRunOptions) {
|
|
180
|
+
const [realm, collection, ...rest] = (method ?? '').split('.').filter(Boolean);
|
|
181
|
+
return {
|
|
182
|
+
realm,
|
|
183
|
+
collection,
|
|
184
|
+
group: rest.length > 0 ? rest.join('.') : undefined,
|
|
185
|
+
logUrl: options.logUrl,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The steps an executor recorded, in the order they completed. */
|
|
190
|
+
function stepsOf(executor: TestExecutor): IStepProgress[] {
|
|
191
|
+
return Array.from(executor.getProgress().steps.values());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Write one group result, standing in for a step list its executor may not have. */
|
|
195
|
+
async function writeGroupResult(
|
|
196
|
+
options: IAllureRunOptions,
|
|
197
|
+
name: string,
|
|
198
|
+
executor: TestExecutor,
|
|
199
|
+
method?: string,
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
const steps = stepsOf(executor);
|
|
202
|
+
if (steps.length === 0) return;
|
|
203
|
+
const {allureGroupResultWrite} = await import(/* @vite-ignore */ allurePackage());
|
|
204
|
+
await allureGroupResultWrite(
|
|
205
|
+
sessionConfig(options).outputDir,
|
|
206
|
+
{name, steps},
|
|
207
|
+
contextOf(method, options),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* End the Allure session, if one was started, and optionally generate its report.
|
|
213
|
+
*
|
|
214
|
+
* Called by the runner once every group has finished, because the results
|
|
215
|
+
* directory belongs to the run rather than to any one group.
|
|
216
|
+
*/
|
|
217
|
+
export async function endAllureSession(): Promise<void> {
|
|
218
|
+
if (session === undefined) return;
|
|
219
|
+
const options = sessionOptions ?? {};
|
|
220
|
+
const closing = session;
|
|
221
|
+
session = undefined;
|
|
222
|
+
sessionOptions = undefined;
|
|
223
|
+
// Waiting on the start first: the end must not run the CLI over a results
|
|
224
|
+
// directory while the thing that lays it out is still opening it. A start that
|
|
225
|
+
// failed has already cleared `session` (see `ensureSession`), so this awaits one
|
|
226
|
+
// that opened, or that is opening.
|
|
227
|
+
await closing;
|
|
228
|
+
const {allureSessionEnd} = await import(/* @vite-ignore */ allurePackage());
|
|
229
|
+
await allureSessionEnd({...sessionConfig(options), generateOnEnd: options.generateOnEnd});
|
|
230
|
+
}
|
|
231
|
+
|
|
15
232
|
const runSteps =
|
|
16
233
|
(
|
|
17
234
|
steps: Steps,
|
|
18
235
|
log?: ITestLogger,
|
|
236
|
+
options: IChainOptions = {},
|
|
19
237
|
results = {$meta: {}},
|
|
20
238
|
): ((t: ITestContext) => Promise<void>) =>
|
|
21
239
|
async (t: ITestContext) => {
|
|
@@ -33,6 +251,11 @@ const runSteps =
|
|
|
33
251
|
resolvedSteps.push(await stepPromise);
|
|
34
252
|
}
|
|
35
253
|
|
|
254
|
+
// Reporting is a run-level concern, and this call never throws: a report that
|
|
255
|
+
// cannot be written says so once and the run carries on being judged on its
|
|
256
|
+
// tests (see `ensureSession`).
|
|
257
|
+
const reporting = await ensureSession(options.allure, options.platform, log);
|
|
258
|
+
|
|
36
259
|
// Execute with parallel executor, passing test context for nested output
|
|
37
260
|
try {
|
|
38
261
|
await withProgress(
|
|
@@ -57,6 +280,15 @@ const runSteps =
|
|
|
57
280
|
} catch (error) {
|
|
58
281
|
// Preserve error with context
|
|
59
282
|
throw error;
|
|
283
|
+
} finally {
|
|
284
|
+
// The group is the test case: one result per group, written whether the
|
|
285
|
+
// group passed or not — a scenario that failed is the one a reader opens
|
|
286
|
+
// the report for. The group's own name is what it was run as.
|
|
287
|
+
if (reporting && options.allure) {
|
|
288
|
+
await writeGroupResult(options.allure, steps.name, executor, options.method).catch(
|
|
289
|
+
error => log?.error?.(error),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
60
292
|
}
|
|
61
293
|
};
|
|
62
294
|
|
|
@@ -92,8 +324,12 @@ const runSteps =
|
|
|
92
324
|
export default async (
|
|
93
325
|
test: ITestContext,
|
|
94
326
|
log?: ITestLogger,
|
|
327
|
+
options: IChainOptions = {},
|
|
95
328
|
): Promise<(steps: Steps) => unknown> => {
|
|
96
329
|
const context = test || (await import('node:test')).default;
|
|
97
330
|
return steps =>
|
|
98
|
-
context.test(
|
|
331
|
+
context.test(
|
|
332
|
+
steps.name,
|
|
333
|
+
runSteps(steps, log, options) as (t: unknown) => void | Promise<void>,
|
|
334
|
+
);
|
|
99
335
|
};
|