@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.
Files changed (29) hide show
  1. package/lib/langium/diagnostics/logger.d.ts +9 -1
  2. package/lib/langium/diagnostics/logger.d.ts.map +1 -1
  3. package/lib/langium/diagnostics/logger.js +24 -6
  4. package/lib/langium/diagnostics/logger.js.map +1 -1
  5. package/lib/langium/workspace/hydranium-workspace-manager.d.ts +4 -3
  6. package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
  7. package/lib/langium/workspace/hydranium-workspace-manager.js +7 -6
  8. package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
  9. package/lib/node/index.d.ts.map +1 -1
  10. package/lib/node/index.js +4 -0
  11. package/lib/node/index.js.map +1 -1
  12. package/lib/node/startup-cpu-profile.d.ts +29 -0
  13. package/lib/node/startup-cpu-profile.d.ts.map +1 -0
  14. package/lib/node/startup-cpu-profile.js +91 -0
  15. package/lib/node/startup-cpu-profile.js.map +1 -0
  16. package/lib/testing/playwright/server-log-capture.d.ts +25 -12
  17. package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
  18. package/lib/testing/playwright/server-log-capture.js +94 -39
  19. package/lib/testing/playwright/server-log-capture.js.map +1 -1
  20. package/lib/testing/playwright/server-log-rename-reporter.d.ts.map +1 -1
  21. package/lib/testing/playwright/server-log-rename-reporter.js +4 -3
  22. package/lib/testing/playwright/server-log-rename-reporter.js.map +1 -1
  23. package/package.json +6 -6
  24. package/src/langium/diagnostics/logger.ts +26 -6
  25. package/src/langium/workspace/hydranium-workspace-manager.ts +7 -6
  26. package/src/node/index.ts +4 -0
  27. package/src/node/startup-cpu-profile.ts +94 -0
  28. package/src/testing/playwright/server-log-capture.ts +98 -41
  29. 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) and `<token>.browser.log` (forwarded
180
- * browser console) to the spec name recorded in its `<token>.spec` sidecar
181
- * (written by {@link markServerLog}), then delete the sidecar. Colliding names —
182
- * a spec that opened more than one workspace — get a `-2`, `-3`, … suffix. Run
183
- * once, after all server processes have exited (e.g. a reporter's `onEnd`).
184
- * Best-effort.
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 the capture wrote that
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
- // Per suffix (`.log`, `.browser.log`) a `taken` set guards collisions, so the
206
- // backend and browser logs for one spec share the same friendly base name.
207
- const taken: Record<string, Set<string>> = { '.log': new Set(), '.browser.log': new Set() };
208
- const rename = (token: string, name: string, suffix: string): void => {
209
- const source = join(dir, `${token}${suffix}`);
210
- if (!existsSync(source)) {
211
- return;
212
- }
213
- let target = `${name}${suffix}`;
214
- for (let n = 2; taken[suffix].has(target) || existsSync(join(dir, target)); n++) {
215
- target = `${name}-${n}${suffix}`;
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
- taken[suffix].add(target);
218
- try {
219
- renameSync(source, join(dir, target));
220
- } catch {
221
- // ignore
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 for, so the ones no sidecar
225
- // claims can be reported rather than silently left behind.
226
- const unattributed = new Set(
227
- entries.filter(entry => entry.endsWith('.log') && !entry.endsWith('.browser.log')).map(entry => entry.slice(0, -'.log'.length))
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
- rename(token, name, '.log');
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
- * Which outcomes attach is {@link AttachServerLogOptions.attachOn}, failure-only
441
- * by default.
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 both streams: the backend server log and (when present) the separate
470
- // browser-console log, so a failing test carries the frontend side too.
471
- const attachments: { name: string; path: string }[] = [
472
- { name: 'server-log', path: file },
473
- { name: 'browser-console-log', path: join(dir, `${toLogFileWorkspaceToken(workspacePath)}.browser.log`) }
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: 'text/plain' });
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} captured log(s) belong to no test: ${unattributed.join(', ')}.\n` +
35
- '[server-log] Those specs never marked a boundary — apply the `serverLog` fixture, ' +
36
- 'or call `markServerLog`/`attachServerLog` from the suite, or their logs cannot be read per test.'
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
  }