@hydranium/core 1.0.0-next.288 → 1.0.0-next.289
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/lib/langium/diagnostics/logger.d.ts +9 -1
- package/lib/langium/diagnostics/logger.d.ts.map +1 -1
- package/lib/langium/diagnostics/logger.js +24 -6
- package/lib/langium/diagnostics/logger.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts +4 -3
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.js +7 -6
- package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +4 -0
- package/lib/node/index.js.map +1 -1
- package/lib/node/startup-cpu-profile.d.ts +29 -0
- package/lib/node/startup-cpu-profile.d.ts.map +1 -0
- package/lib/node/startup-cpu-profile.js +91 -0
- package/lib/node/startup-cpu-profile.js.map +1 -0
- package/lib/testing/playwright/server-log-capture.d.ts +25 -12
- package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-capture.js +94 -39
- package/lib/testing/playwright/server-log-capture.js.map +1 -1
- package/lib/testing/playwright/server-log-rename-reporter.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-rename-reporter.js +4 -3
- package/lib/testing/playwright/server-log-rename-reporter.js.map +1 -1
- package/package.json +6 -6
- package/src/langium/diagnostics/logger.ts +26 -6
- package/src/langium/workspace/hydranium-workspace-manager.ts +7 -6
- package/src/node/index.ts +4 -0
- package/src/node/startup-cpu-profile.ts +94 -0
- package/src/testing/playwright/server-log-capture.ts +98 -41
- package/src/testing/playwright/server-log-rename-reporter.ts +4 -3
|
@@ -11,6 +11,7 @@ import { appendFileSync, existsSync, readdirSync, readFileSync, renameSync, unli
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import {
|
|
14
|
+
DEFAULT_CPU_PROFILE_FILE_ENV,
|
|
14
15
|
DEFAULT_LOG_FILE_ENV,
|
|
15
16
|
DEFAULT_LOG_FILE_LEVEL_ENV,
|
|
16
17
|
DEFAULT_LOG_LEVEL_ENV,
|
|
@@ -26,6 +27,13 @@ import { toLogFileWorkspaceToken } from '../../langium/diagnostics/log-file-toke
|
|
|
26
27
|
* {@link attachServerLog}.
|
|
27
28
|
*/
|
|
28
29
|
export const DEFAULT_SERVER_LOG_DIR_ENV = 'HYDRANIUM_SERVER_LOG_DIR';
|
|
30
|
+
/**
|
|
31
|
+
* Default env var opting a capture into the servers' CPU profiles: any
|
|
32
|
+
* non-empty value makes {@link captureServerLog} name a
|
|
33
|
+
* `<token>.<pid>.cpuprofile` beside each log, which each server of that
|
|
34
|
+
* workspace writes when it ends.
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_SERVER_CPU_PROFILE_ENV = 'HYDRANIUM_SERVER_CPU_PROFILE';
|
|
29
37
|
/**
|
|
30
38
|
* Default env var providing a central {@link AttachServerLogOptions.attachOn}
|
|
31
39
|
* policy (`'failure' | 'always' | 'never'`) for a whole run, so a debugging
|
|
@@ -65,6 +73,26 @@ function parseBrowserLogLevels(value: string | undefined): readonly string[] | u
|
|
|
65
73
|
|
|
66
74
|
/** The `{workspace}` placeholder the file-tee expands once the workspace is known. */
|
|
67
75
|
const WORKSPACE_PLACEHOLDER = '{workspace}';
|
|
76
|
+
/**
|
|
77
|
+
* Name of each server's CPU profile beside `<token>.log`. Per process, since
|
|
78
|
+
* every server of a workspace writes one and a write replaces the file.
|
|
79
|
+
*/
|
|
80
|
+
const CPU_PROFILE_NAME = `${WORKSPACE_PLACEHOLDER}.{pid}.cpuprofile`;
|
|
81
|
+
|
|
82
|
+
/** Matches the `.<pid>.cpuprofile` ending of a profile named by {@link CPU_PROFILE_NAME}. */
|
|
83
|
+
const CPU_PROFILE_SUFFIX_PATTERN = /\.\d+\.cpuprofile$/;
|
|
84
|
+
|
|
85
|
+
/** The `.<pid>.cpuprofile` suffixes of the profiles among `entries` written for `token`. */
|
|
86
|
+
function cpuProfileSuffixes(entries: readonly string[], token: string): string[] {
|
|
87
|
+
const suffixes: string[] = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const suffix = CPU_PROFILE_SUFFIX_PATTERN.exec(entry)?.[0];
|
|
90
|
+
if (suffix !== undefined && entry.slice(0, -suffix.length) === token) {
|
|
91
|
+
suffixes.push(suffix);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return suffixes.sort();
|
|
95
|
+
}
|
|
68
96
|
|
|
69
97
|
export interface CaptureServerLogOptions {
|
|
70
98
|
/**
|
|
@@ -112,7 +140,8 @@ export interface ServerLogRenameReporterOptions {
|
|
|
112
140
|
* plus the reporter entry that renames those files to spec names once the run
|
|
113
141
|
* ends. Returns an **empty env, and no reporter, when no dir is configured**, so a
|
|
114
142
|
* normal run is untouched. The `{workspace}` token is expanded server-side once
|
|
115
|
-
* the workspace is known.
|
|
143
|
+
* the workspace is known. With {@link DEFAULT_SERVER_CPU_PROFILE_ENV} set, the
|
|
144
|
+
* env also names a `<token>.<pid>.cpuprofile` per server beside each log.
|
|
116
145
|
*
|
|
117
146
|
* **Publishes a resolved `options.dir` onto {@link DEFAULT_SERVER_LOG_DIR_ENV} in
|
|
118
147
|
* the CALLING process, and that mutation is load-bearing rather than a
|
|
@@ -139,6 +168,9 @@ export function captureServerLog(options: CaptureServerLogOptions = {}): Capture
|
|
|
139
168
|
parseLogLevel(process.env[DEFAULT_LOG_FILE_LEVEL_ENV]) ??
|
|
140
169
|
parseLogLevel(process.env[DEFAULT_LOG_LEVEL_ENV]) ??
|
|
141
170
|
'debug';
|
|
171
|
+
if (process.env[DEFAULT_SERVER_CPU_PROFILE_ENV]) {
|
|
172
|
+
env[DEFAULT_CPU_PROFILE_FILE_ENV] = join(dir, CPU_PROFILE_NAME);
|
|
173
|
+
}
|
|
142
174
|
const reporterModule = fileURLToPath(new URL('./server-log-rename-reporter.js', import.meta.url));
|
|
143
175
|
return { env, reporter: [reporterModule, { dir }] };
|
|
144
176
|
}
|
|
@@ -176,16 +208,18 @@ export function serverLogSpecName(file: string): string {
|
|
|
176
208
|
}
|
|
177
209
|
|
|
178
210
|
/**
|
|
179
|
-
* Rename each `<token>.log` (backend)
|
|
180
|
-
* browser console)
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
211
|
+
* Rename each `<token>.log` (backend), `<token>.browser.log` (forwarded
|
|
212
|
+
* browser console) and `<token>.<pid>.cpuprofile` (each server's CPU profile,
|
|
213
|
+
* when {@link DEFAULT_SERVER_CPU_PROFILE_ENV} is set) to the spec name recorded
|
|
214
|
+
* in its `<token>.spec` sidecar (written by {@link markServerLog}), then delete
|
|
215
|
+
* the sidecar. Colliding names — a spec that opened more than one workspace —
|
|
216
|
+
* get a `-2`, `-3`, … suffix. Run once, after all server processes have exited
|
|
217
|
+
* (e.g. a reporter's `onEnd`). Best-effort.
|
|
185
218
|
*
|
|
186
|
-
* Returns the workspace tokens left UNATTRIBUTED — a log
|
|
187
|
-
* no test claimed, because that spec never reached
|
|
188
|
-
* {@link markServerLog} (no `serverLog` fixture, no `beforeEach` call)
|
|
219
|
+
* Returns the workspace tokens left UNATTRIBUTED — a log or CPU profile the
|
|
220
|
+
* capture wrote that no test claimed, because that spec never reached
|
|
221
|
+
* {@link markServerLog} (no `serverLog` fixture, no `beforeEach` call), or
|
|
222
|
+
* because its server never learned its workspace and named it `_startup-<pid>`.
|
|
189
223
|
*
|
|
190
224
|
* **Returned rather than ignored, because the unattributed state is the
|
|
191
225
|
* misleading one.** Such a log still exists, still has content, and is named by
|
|
@@ -202,30 +236,38 @@ export function renameServerLogs(dir: string): string[] {
|
|
|
202
236
|
// unattributed, since no log was observed at all.
|
|
203
237
|
return [];
|
|
204
238
|
}
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
let
|
|
214
|
-
for (let n = 2;
|
|
215
|
-
|
|
239
|
+
// One base name per token, free for every file the token has, so a spec's
|
|
240
|
+
// second workspace keeps its log, browser log and profiles paired under one
|
|
241
|
+
// `-N`. A base chosen per file pairs nothing: a profile's suffix carries its
|
|
242
|
+
// pid and never collides, so it would stay at the plain name beside a `-2` log.
|
|
243
|
+
const renameAll = (token: string, name: string): void => {
|
|
244
|
+
const suffixes = ['.log', '.browser.log', ...cpuProfileSuffixes(entries, token)].filter(suffix =>
|
|
245
|
+
existsSync(join(dir, `${token}${suffix}`))
|
|
246
|
+
);
|
|
247
|
+
let base = name;
|
|
248
|
+
for (let n = 2; suffixes.some(suffix => existsSync(join(dir, `${base}${suffix}`))); n++) {
|
|
249
|
+
base = `${name}-${n}`;
|
|
216
250
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
251
|
+
for (const suffix of suffixes) {
|
|
252
|
+
try {
|
|
253
|
+
renameSync(join(dir, `${token}${suffix}`), join(dir, `${base}${suffix}`));
|
|
254
|
+
} catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
222
257
|
}
|
|
223
258
|
};
|
|
224
|
-
// Every token the capture produced a backend log
|
|
225
|
-
// claims can be reported rather than silently left behind.
|
|
226
|
-
const unattributed = new Set(
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
// Every token the capture produced a backend log or a CPU profile for, so
|
|
260
|
+
// the ones no sidecar claims can be reported rather than silently left behind.
|
|
261
|
+
const unattributed = new Set<string>();
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
if (entry.endsWith('.log') && !entry.endsWith('.browser.log')) {
|
|
264
|
+
unattributed.add(entry.slice(0, -'.log'.length));
|
|
265
|
+
}
|
|
266
|
+
const profileSuffix = CPU_PROFILE_SUFFIX_PATTERN.exec(entry)?.[0];
|
|
267
|
+
if (profileSuffix !== undefined) {
|
|
268
|
+
unattributed.add(entry.slice(0, -profileSuffix.length));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
229
271
|
for (const entry of entries) {
|
|
230
272
|
if (!entry.endsWith('.spec')) {
|
|
231
273
|
continue;
|
|
@@ -247,8 +289,7 @@ export function renameServerLogs(dir: string): string[] {
|
|
|
247
289
|
continue;
|
|
248
290
|
}
|
|
249
291
|
unattributed.delete(token);
|
|
250
|
-
|
|
251
|
-
rename(token, name, '.browser.log');
|
|
292
|
+
renameAll(token, name);
|
|
252
293
|
}
|
|
253
294
|
return [...unattributed];
|
|
254
295
|
}
|
|
@@ -437,8 +478,11 @@ export function findUnexpectedServerLogLines(text: string, allowlist: Readonly<R
|
|
|
437
478
|
* server log, so each test's region is delimited within the shared stream;
|
|
438
479
|
* - attaches that log — and the separate browser-console log, when one exists —
|
|
439
480
|
* to the test, so it shows in the HTML report alongside the trace + screenshot.
|
|
440
|
-
*
|
|
441
|
-
*
|
|
481
|
+
* Every CPU profile of the workspace's servers written by then is attached
|
|
482
|
+
* too, so one of a server that ended in an earlier test of the same
|
|
483
|
+
* workspace comes along, and one written after the test is only named after
|
|
484
|
+
* the spec by {@link renameServerLogs}. Which outcomes attach is
|
|
485
|
+
* {@link AttachServerLogOptions.attachOn}, failure-only by default.
|
|
442
486
|
*
|
|
443
487
|
* **No-op when no capture dir is configured** (`options.dir` and the default env
|
|
444
488
|
* var both unset), so a normal run is untouched — no per-spec guard needed.
|
|
@@ -466,18 +510,31 @@ export async function attachServerLog(
|
|
|
466
510
|
if (!shouldAttach) {
|
|
467
511
|
return;
|
|
468
512
|
}
|
|
469
|
-
// Attach
|
|
470
|
-
// browser-console log, so a failing test carries the frontend side too
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
513
|
+
// Attach every stream present: the backend server log, the separate
|
|
514
|
+
// browser-console log, so a failing test carries the frontend side too, and
|
|
515
|
+
// the CPU profile of each server of the workspace that has already ended.
|
|
516
|
+
const token = toLogFileWorkspaceToken(workspacePath);
|
|
517
|
+
let entries: string[] = [];
|
|
518
|
+
try {
|
|
519
|
+
entries = readdirSync(dir);
|
|
520
|
+
} catch {
|
|
521
|
+
// No profiles to attach; the logs below are checked one by one.
|
|
522
|
+
}
|
|
523
|
+
const attachments: { name: string; path: string; contentType: string }[] = [
|
|
524
|
+
{ name: 'server-log', path: file, contentType: 'text/plain' },
|
|
525
|
+
{ name: 'browser-console-log', path: join(dir, `${token}.browser.log`), contentType: 'text/plain' },
|
|
526
|
+
...cpuProfileSuffixes(entries, token).map(suffix => ({
|
|
527
|
+
name: 'server-cpu-profile',
|
|
528
|
+
path: join(dir, `${token}${suffix}`),
|
|
529
|
+
contentType: 'application/json'
|
|
530
|
+
}))
|
|
474
531
|
];
|
|
475
532
|
for (const attachment of attachments) {
|
|
476
533
|
if (!existsSync(attachment.path)) {
|
|
477
534
|
continue;
|
|
478
535
|
}
|
|
479
536
|
try {
|
|
480
|
-
await testInfo.attach(attachment.name, { path: attachment.path, contentType:
|
|
537
|
+
await testInfo.attach(attachment.name, { path: attachment.path, contentType: attachment.contentType });
|
|
481
538
|
} catch {
|
|
482
539
|
// Attaching must not break the test run.
|
|
483
540
|
}
|
|
@@ -31,9 +31,10 @@ export default class ServerLogRenameReporter {
|
|
|
31
31
|
// opaque workspace token and carry no test boundaries, so nothing
|
|
32
32
|
// about them says which spec they belong to.
|
|
33
33
|
console.warn(
|
|
34
|
-
`[server-log] ${unattributed.length}
|
|
35
|
-
'[server-log]
|
|
36
|
-
'or call `markServerLog`/`attachServerLog` from the suite, or
|
|
34
|
+
`[server-log] ${unattributed.length} workspace(s) left captured logs or profiles that belong to no test: ${unattributed.join(', ')}.\n` +
|
|
35
|
+
'[server-log] A workspace token means its spec never marked a boundary — apply the `serverLog` fixture, ' +
|
|
36
|
+
'or call `markServerLog`/`attachServerLog` from the suite, or its logs cannot be read per test. ' +
|
|
37
|
+
'A `_startup-<pid>` name is a server that never learned its workspace, so no spec can claim its files.'
|
|
37
38
|
);
|
|
38
39
|
}
|
|
39
40
|
}
|