From 82e548a61165796e384787f07f30e9b536945950 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Jan 2025 13:18:13 +0100 Subject: [PATCH 1/5] chore: fix memory overflow for buffered console Since the buffered console captures stdout/stderr, in some call sequences it keeps recursing forever and overflows memory. It does not repro in this repository, but it repros in a different one. The fix is to stop capturing while we print results. --- .../testhelpers/jest-bufferedconsole.ts | 17 +++++++++++++++-- packages/aws-cdk/test/jest-bufferedconsole.ts | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts index a81ddb4282e57..c22eb6f02faf4 100644 --- a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts +++ b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts @@ -26,11 +26,15 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi // doesn't work properly. (this as JestEnvironment).handleTestEvent = (async (event, _state) => { if (event.name === 'test_done' && event.test.errors.length > 0 && this.log.length > 0) { + this.stopCapture(); + this.originalConsole.log(`[Console output] ${fullTestName(event.test)}\n`); for (const item of this.log) { this.originalConsole[item.type](' ' + item.message); } this.originalConsole.log('\n'); + + this.startCapture(); } if (event.name === 'test_done') { @@ -43,6 +47,15 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi await super.setup(); this.log = []; + this.startCapture(); + } + + async teardown() { + this.stopCapture(); + await super.teardown(); + } + + private startCapture() { this.originalConsole = console; this.originalStdoutWrite = process.stdout.write; this.originalStderrWrite = process.stderr.write; @@ -66,6 +79,7 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi } } as any; process.stderr.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { + self.originalStderrWrite(chunk); const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; self.log.push({ type: 'error', message: message.replace(/\n$/, '') }); @@ -75,11 +89,10 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi } as any; } - async teardown() { + private stopCapture() { this.global.console = this.originalConsole; process.stdout.write = this.originalStdoutWrite; process.stderr.write = this.originalStderrWrite; - await super.teardown(); } } diff --git a/packages/aws-cdk/test/jest-bufferedconsole.ts b/packages/aws-cdk/test/jest-bufferedconsole.ts index a81ddb4282e57..c22eb6f02faf4 100644 --- a/packages/aws-cdk/test/jest-bufferedconsole.ts +++ b/packages/aws-cdk/test/jest-bufferedconsole.ts @@ -26,11 +26,15 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi // doesn't work properly. (this as JestEnvironment).handleTestEvent = (async (event, _state) => { if (event.name === 'test_done' && event.test.errors.length > 0 && this.log.length > 0) { + this.stopCapture(); + this.originalConsole.log(`[Console output] ${fullTestName(event.test)}\n`); for (const item of this.log) { this.originalConsole[item.type](' ' + item.message); } this.originalConsole.log('\n'); + + this.startCapture(); } if (event.name === 'test_done') { @@ -43,6 +47,15 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi await super.setup(); this.log = []; + this.startCapture(); + } + + async teardown() { + this.stopCapture(); + await super.teardown(); + } + + private startCapture() { this.originalConsole = console; this.originalStdoutWrite = process.stdout.write; this.originalStderrWrite = process.stderr.write; @@ -66,6 +79,7 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi } } as any; process.stderr.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { + self.originalStderrWrite(chunk); const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; self.log.push({ type: 'error', message: message.replace(/\n$/, '') }); @@ -75,11 +89,10 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi } as any; } - async teardown() { + private stopCapture() { this.global.console = this.originalConsole; process.stdout.write = this.originalStdoutWrite; process.stderr.write = this.originalStderrWrite; - await super.teardown(); } } From 183815736ef7404ceb4c82fe4921c993e0b1bf67 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Jan 2025 13:43:54 +0100 Subject: [PATCH 2/5] Oopsie --- packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts | 1 - packages/aws-cdk/test/jest-bufferedconsole.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts index c22eb6f02faf4..34c3c31a55522 100644 --- a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts +++ b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts @@ -79,7 +79,6 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi } } as any; process.stderr.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { - self.originalStderrWrite(chunk); const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; self.log.push({ type: 'error', message: message.replace(/\n$/, '') }); diff --git a/packages/aws-cdk/test/jest-bufferedconsole.ts b/packages/aws-cdk/test/jest-bufferedconsole.ts index c22eb6f02faf4..34c3c31a55522 100644 --- a/packages/aws-cdk/test/jest-bufferedconsole.ts +++ b/packages/aws-cdk/test/jest-bufferedconsole.ts @@ -79,7 +79,6 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi } } as any; process.stderr.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { - self.originalStderrWrite(chunk); const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; self.log.push({ type: 'error', message: message.replace(/\n$/, '') }); From 7ebea3298c400b394adf067f9a969e0b67ec219f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Jan 2025 14:05:48 +0100 Subject: [PATCH 3/5] Fix printing mistake --- packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts | 2 +- packages/aws-cdk/test/jest-bufferedconsole.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts index 34c3c31a55522..67fafc8bded08 100644 --- a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts +++ b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts @@ -104,7 +104,7 @@ type PartialBy = Omit & Partial> function fullTestName(test: TestDescription) { let ret = test.name; while (test.parent != null && test.parent.name !== 'ROOT_DESCRIBE_BLOCK') { - ret = test.parent.name + ' › ' + fullTestName; + ret = test.parent.name + ' › ' + ret; test = test.parent; } return ret; diff --git a/packages/aws-cdk/test/jest-bufferedconsole.ts b/packages/aws-cdk/test/jest-bufferedconsole.ts index 34c3c31a55522..67fafc8bded08 100644 --- a/packages/aws-cdk/test/jest-bufferedconsole.ts +++ b/packages/aws-cdk/test/jest-bufferedconsole.ts @@ -104,7 +104,7 @@ type PartialBy = Omit & Partial> function fullTestName(test: TestDescription) { let ret = test.name; while (test.parent != null && test.parent.name !== 'ROOT_DESCRIBE_BLOCK') { - ret = test.parent.name + ' › ' + fullTestName; + ret = test.parent.name + ' › ' + ret; test = test.parent; } return ret; From 424d36bf6764beca7384a7a1d55492b010a5132a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Jan 2025 14:07:56 +0100 Subject: [PATCH 4/5] Istanbul ignore some other stupid nondeterminstic stupid file --- packages/aws-cdk/lib/version.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/aws-cdk/lib/version.ts b/packages/aws-cdk/lib/version.ts index 5b7fee798d80a..bff01ade6b885 100644 --- a/packages/aws-cdk/lib/version.ts +++ b/packages/aws-cdk/lib/version.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import * as path from 'path'; import * as chalk from 'chalk'; import * as fs from 'fs-extra'; From 32fcabc3c579f2131482a11f91f026c88575d9e4 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Jan 2025 14:21:10 +0100 Subject: [PATCH 5/5] Don't assume all the console.log() arguments are strings --- .../testhelpers/jest-bufferedconsole.ts | 18 +++++++++--------- packages/aws-cdk/test/jest-bufferedconsole.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts index 67fafc8bded08..7dac07c7fc7da 100644 --- a/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts +++ b/packages/aws-cdk-lib/testhelpers/jest-bufferedconsole.ts @@ -8,7 +8,7 @@ import { TestEnvironment as NodeEnvironment } from 'jest-environment-node'; interface ConsoleMessage { type: 'log' | 'error' | 'warn' | 'info' | 'debug'; - message: string; + args: any[]; } export default class TestEnvironment extends NodeEnvironment implements JestEnvironment { @@ -30,7 +30,7 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi this.originalConsole.log(`[Console output] ${fullTestName(event.test)}\n`); for (const item of this.log) { - this.originalConsole[item.type](' ' + item.message); + this.originalConsole[item.type].apply(this.originalConsole, [' ', ...item.args]); } this.originalConsole.log('\n'); @@ -62,18 +62,18 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi this.global.console = { ...console, - log: (message) => this.log.push({ type: 'log', message }), - error: (message) => this.log.push({ type: 'error', message }), - warn: (message) => this.log.push({ type: 'warn', message }), - info: (message) => this.log.push({ type: 'info', message }), - debug: (message) => this.log.push({ type: 'debug', message }), + log: (...args) => this.log.push({ type: 'log', args }), + error: (...args) => this.log.push({ type: 'error', args }), + warn: (...args) => this.log.push({ type: 'warn', args }), + info: (...args) => this.log.push({ type: 'info', args }), + debug: (...args) => this.log.push({ type: 'debug', args }), }; const self = this; process.stdout.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; - self.log.push({ type: 'log', message: message.replace(/\n$/, '') }); + self.log.push({ type: 'log', args: [message.replace(/\n$/, '')] }); if (typeof enccb === 'function') { enccb(); } @@ -81,7 +81,7 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi process.stderr.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; - self.log.push({ type: 'error', message: message.replace(/\n$/, '') }); + self.log.push({ type: 'error', args: [message.replace(/\n$/, '')] }); if (typeof enccb === 'function') { enccb(); } diff --git a/packages/aws-cdk/test/jest-bufferedconsole.ts b/packages/aws-cdk/test/jest-bufferedconsole.ts index 67fafc8bded08..7dac07c7fc7da 100644 --- a/packages/aws-cdk/test/jest-bufferedconsole.ts +++ b/packages/aws-cdk/test/jest-bufferedconsole.ts @@ -8,7 +8,7 @@ import { TestEnvironment as NodeEnvironment } from 'jest-environment-node'; interface ConsoleMessage { type: 'log' | 'error' | 'warn' | 'info' | 'debug'; - message: string; + args: any[]; } export default class TestEnvironment extends NodeEnvironment implements JestEnvironment { @@ -30,7 +30,7 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi this.originalConsole.log(`[Console output] ${fullTestName(event.test)}\n`); for (const item of this.log) { - this.originalConsole[item.type](' ' + item.message); + this.originalConsole[item.type].apply(this.originalConsole, [' ', ...item.args]); } this.originalConsole.log('\n'); @@ -62,18 +62,18 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi this.global.console = { ...console, - log: (message) => this.log.push({ type: 'log', message }), - error: (message) => this.log.push({ type: 'error', message }), - warn: (message) => this.log.push({ type: 'warn', message }), - info: (message) => this.log.push({ type: 'info', message }), - debug: (message) => this.log.push({ type: 'debug', message }), + log: (...args) => this.log.push({ type: 'log', args }), + error: (...args) => this.log.push({ type: 'error', args }), + warn: (...args) => this.log.push({ type: 'warn', args }), + info: (...args) => this.log.push({ type: 'info', args }), + debug: (...args) => this.log.push({ type: 'debug', args }), }; const self = this; process.stdout.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; - self.log.push({ type: 'log', message: message.replace(/\n$/, '') }); + self.log.push({ type: 'log', args: [message.replace(/\n$/, '')] }); if (typeof enccb === 'function') { enccb(); } @@ -81,7 +81,7 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi process.stderr.write = function (chunk: Buffer | string, enccb?: BufferEncoding | ((error?: Error | null) => void)): void { const encoding = typeof enccb === 'string' ? enccb : 'utf-8'; const message = Buffer.isBuffer(chunk) ? chunk.toString(encoding) : chunk; - self.log.push({ type: 'error', message: message.replace(/\n$/, '') }); + self.log.push({ type: 'error', args: [message.replace(/\n$/, '')] }); if (typeof enccb === 'function') { enccb(); }