Skip to content

Commit

Permalink
Merge branch 'main' into nlb-dualstack-listener
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Dec 7, 2024
2 parents 3cd9c0a + 2607eb3 commit d92d952
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 81 deletions.
87 changes: 44 additions & 43 deletions packages/aws-cdk-lib/aws-lambda/lib/function.ts

Large diffs are not rendered by default.

7 changes: 2 additions & 5 deletions packages/aws-cdk-lib/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,8 +1074,8 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase {
}

private validateServerlessScalingConfig(): void {
if (this.serverlessV2MaxCapacity > 256 || this.serverlessV2MaxCapacity < 0.5) {
throw new Error('serverlessV2MaxCapacity must be >= 0.5 & <= 256');
if (this.serverlessV2MaxCapacity > 256 || this.serverlessV2MaxCapacity < 1) {
throw new Error('serverlessV2MaxCapacity must be >= 1 & <= 256');
}

if (this.serverlessV2MinCapacity > 256 || this.serverlessV2MinCapacity < 0) {
Expand All @@ -1086,9 +1086,6 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase {
throw new Error('serverlessV2MaxCapacity must be greater than serverlessV2MinCapacity');
}

if (this.serverlessV2MaxCapacity === 0.5 && this.serverlessV2MinCapacity === 0.5) {
throw new Error('If serverlessV2MinCapacity === 0.5 then serverlessV2MaxCapacity must be >=1');
}
const regexp = new RegExp(/^[0-9]+\.?5?$/);
if (!regexp.test(this.serverlessV2MaxCapacity.toString()) || !regexp.test(this.serverlessV2MinCapacity.toString())) {
throw new Error('serverlessV2MinCapacity & serverlessV2MaxCapacity must be in 0.5 step increments, received '+
Expand Down
5 changes: 2 additions & 3 deletions packages/aws-cdk-lib/aws-rds/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,10 @@ describe('cluster new api', () => {
});

test.each([
[0.5, 300, /serverlessV2MaxCapacity must be >= 0.5 & <= 256/],
[0.5, 0, /serverlessV2MaxCapacity must be >= 0.5 & <= 256/],
[0.5, 300, /serverlessV2MaxCapacity must be >= 1 & <= 256/],
[0.5, 0, /serverlessV2MaxCapacity must be >= 1 & <= 256/],
[-1, 1, /serverlessV2MinCapacity must be >= 0 & <= 256/],
[300, 1, /serverlessV2MinCapacity must be >= 0 & <= 256/],
[0.5, 0.5, /If serverlessV2MinCapacity === 0.5 then serverlessV2MaxCapacity must be >=1/],
[10.1, 12, /serverlessV2MinCapacity & serverlessV2MaxCapacity must be in 0.5 step increments/],
[12, 12.1, /serverlessV2MinCapacity & serverlessV2MaxCapacity must be in 0.5 step increments/],
[5, 1, /serverlessV2MaxCapacity must be greater than serverlessV2MinCapacity/],
Expand Down
145 changes: 145 additions & 0 deletions packages/aws-cdk-lib/core/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { IConstruct } from 'constructs';
import { constructInfoFromConstruct } from './helpers-internal';

const CONSTRUCT_ERROR_SYMBOL = Symbol.for('@aws-cdk/core.SynthesisError');
const VALIDATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/core.ValidationError');

/**
* Helper to check if an error is a SynthesisErrors
*/
export class Errors {
/**
* Test whether the given errors is a ConstructionError
*/
public static isConstructError(x: any): x is ConstructError {
return x !== null && typeof(x) === 'object' && CONSTRUCT_ERROR_SYMBOL in x;
}

/**
* Test whether the given error is a ValidationError
*/
public static isValidationError(x: any): x is ValidationError {
return Errors.isConstructError(x) && VALIDATION_ERROR_SYMBOL in x;
}
}

interface ConstructInfo {
readonly fqn: string;
readonly version: string;
}

/**
* Generic, abstract error that is thrown from the users app during app construction or synth.
*/
abstract class ConstructError extends Error {
#time: string;
#constructPath?: string;
#constructInfo?: ConstructInfo;

/**
* The time the error was thrown.
*/
public get time(): string {
return this.#time;
}

/**
* The level. Always `'error'`.
*/
public get level(): 'error' {
return 'error';
}

/**
* The type of the error.
*/
public abstract get type(): string;

/**
* The path of the construct this error is thrown from, if available.
*/
public get constructPath(): string | undefined {
return this.#constructPath;
}

/**
* Information on the construct this error is thrown from, if available.
*/
public get constructInfo(): ConstructInfo | undefined {
return this.#constructInfo;
}

constructor(msg: string, scope?: IConstruct) {
super(msg);
Object.setPrototypeOf(this, ConstructError.prototype);
Object.defineProperty(this, CONSTRUCT_ERROR_SYMBOL, { value: true });

this.name = new.target.name;
this.#time = new Date().toISOString();
this.#constructPath = scope?.node.path;

if (scope) {
Error.captureStackTrace(this, scope.constructor);
try {
this.#constructInfo = scope ? constructInfoFromConstruct(scope) : undefined;
} catch (_) {
// we don't want to fail if construct info is not available
}
}

const stack = [
`${this.name}: ${this.message}`,
];

if (this.constructInfo) {
stack.push(`in ${this.constructInfo?.fqn} at [${this.constructPath}]`);
} else {
stack.push(`in [${this.constructPath}]`);
}

if (this.stack) {
stack.push(this.stack);
}

this.stack = this.constructStack(this.stack);
}

/**
* Helper message to clean-up the stack and amend with construct information.
*/
private constructStack(prev?: string) {
const indent = ' '.repeat(4);

const stack = [
`${this.name}: ${this.message}`,
];

if (this.constructInfo) {
stack.push(`${indent}at path [${this.constructPath}] in ${this.constructInfo?.fqn}`);
} else {
stack.push(`${indent}at path [${this.constructPath}]`);
}

if (prev) {
stack.push('');
stack.push(...prev.split('\n').slice(1));
}

return stack.join('\n');
}
}

/**
* An Error that is thrown when a construct has validation errors.
*/
export class ValidationError extends ConstructError {
public get type(): 'validation' {
return 'validation';
}

constructor(msg: string, scope: IConstruct) {
super(msg, scope);
Object.setPrototypeOf(this, ValidationError.prototype);
Object.defineProperty(this, VALIDATION_ERROR_SYMBOL, { value: true });
}
}
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export * from './expiration';
export * from './size';
export * from './stack-trace';
export { Element } from './deps';
export { Errors } from './errors';

export * from './app';
export * from './context-provider';
Expand Down
34 changes: 34 additions & 0 deletions packages/aws-cdk-lib/core/test/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { App, Stack } from '../lib';
import { Errors, ValidationError } from '../lib/errors';

jest
.useFakeTimers()
.setSystemTime(new Date('2020-01-01'));

describe('ValidationError', () => {
test('ValidationError is ValidationError and ConstructError', () => {
const error = new ValidationError('this is an error', new App());

expect(Errors.isConstructError(error)).toBe(true);
expect(Errors.isValidationError(error)).toBe(true);
});

test('ValidationError data', () => {
const app = new App();
const stack = new Stack(app, 'MyStack');
const error = new ValidationError('this is an error', stack);

expect(error.time).toBe('2020-01-01T00:00:00.000Z');
expect(error.type).toBe('validation');
expect(error.level).toBe('error');
expect(error.constructPath).toBe('MyStack');
expect(error.constructInfo).toMatchObject({
// fqns are different when run from compiled JS (first) and dynamically from TS (second)
fqn: expect.stringMatching(/aws-cdk-lib\.Stack|constructs\.Construct/),
version: expect.stringMatching(/^\d+\.\d+\.\d+$/),
});
expect(error.message).toBe('this is an error');
expect(error.stack).toContain('ValidationError: this is an error');
expect(error.stack).toContain('at path [MyStack] in');
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { LogLevel, log, setLogLevel, setCI, data, print, error, warning, success, debug, trace, prefix, withCorkedLogging } from '../lib/logging';
import { LogLevel, log, setLogLevel, setCI, data, print, error, warning, success, debug, trace, prefix, withCorkedLogging } from '../../../lib/logging';

describe('logging', () => {
// Mock streams to capture output
let mockStdout: jest.Mock;
let mockStderr: jest.Mock;

// Helper function to strip ANSI codes
const stripAnsi = (str: string): string => {
const ansiRegex = /\u001b\[[0-9;]*[a-zA-Z]/g;
return str.replace(ansiRegex, '');
};

beforeEach(() => {
// Reset log level before each test
setLogLevel(LogLevel.INFO);
Expand All @@ -14,44 +20,45 @@ describe('logging', () => {
mockStdout = jest.fn();
mockStderr = jest.fn();

// Mock the write methods directly
// Mock the write methods directly and strip ANSI codes
jest.spyOn(process.stdout, 'write').mockImplementation((chunk: any) => {
mockStdout(chunk.toString());
mockStdout(stripAnsi(chunk.toString()));
return true;
});

jest.spyOn(process.stderr, 'write').mockImplementation((chunk: any) => {
mockStderr(chunk.toString());
mockStderr(stripAnsi(chunk.toString()));
return true;
});
});

afterEach(() => {
jest.restoreAllMocks();
});

describe('stream selection', () => {
test('data() always writes to stdout', () => {
data('test message');
expect(mockStdout).toHaveBeenCalledWith(expect.stringContaining('test message\n'));
expect(mockStdout).toHaveBeenCalledWith('test message\n');
expect(mockStderr).not.toHaveBeenCalled();
});

test('error() always writes to stderr', () => {
error('test error');
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('test error\n'));
expect(mockStderr).toHaveBeenCalledWith('test error\n');
expect(mockStdout).not.toHaveBeenCalled();
});

test('print() writes to stderr by default', () => {
print('test print');
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('test print\n'));
expect(mockStderr).toHaveBeenCalledWith('test print\n');
expect(mockStdout).not.toHaveBeenCalled();
});

test('print() writes to stdout in CI mode', () => {
setCI(true);
print('test print');
expect(mockStdout).toHaveBeenCalledWith(expect.stringContaining('test print\n'));
expect(mockStdout).toHaveBeenCalledWith('test print\n');
expect(mockStderr).not.toHaveBeenCalled();
});
});
Expand All @@ -62,9 +69,9 @@ describe('logging', () => {
error('error message');
warning('warning message');
print('print message');
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('error message\n'));
expect(mockStderr).not.toHaveBeenCalledWith(expect.stringContaining('warning message\n'));
expect(mockStderr).not.toHaveBeenCalledWith(expect.stringContaining('print message\n'));
expect(mockStderr).toHaveBeenCalledWith('error message\n');
expect(mockStderr).not.toHaveBeenCalledWith('warning message\n');
expect(mockStderr).not.toHaveBeenCalledWith('print message\n');
});

test('debug messages only show at debug level', () => {
Expand All @@ -74,7 +81,7 @@ describe('logging', () => {

setLogLevel(LogLevel.DEBUG);
debug('debug message');
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('debug message\n'));
expect(mockStderr).toHaveBeenCalledWith('debug message\n');
});

test('trace messages only show at trace level', () => {
Expand All @@ -84,26 +91,25 @@ describe('logging', () => {

setLogLevel(LogLevel.TRACE);
trace('trace message');
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('trace message\n'));
expect(mockStderr).toHaveBeenCalledWith('trace message\n');
});
});

describe('message formatting', () => {
test('formats messages with multiple arguments', () => {
print('Value: %d, String: %s', 42, 'test');
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('Value: 42, String: test\n'));
expect(mockStderr).toHaveBeenCalledWith('Value: 42, String: test\n');
});

test('handles prefix correctly', () => {
const prefixedLog = prefix('PREFIX');
prefixedLog('test message');
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('PREFIX test message\n'));
expect(mockStderr).toHaveBeenCalledWith('PREFIX test message\n');
});

test('handles custom styles', () => {
success('success message');
// Note: actual styling will depend on chalk, but we can verify the message is there
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('success message\n'));
expect(mockStderr).toHaveBeenCalledWith('success message\n');
});
});

Expand All @@ -115,8 +121,8 @@ describe('logging', () => {
expect(mockStderr).not.toHaveBeenCalled();
});

expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('message 1\n'));
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('message 2\n'));
expect(mockStderr).toHaveBeenCalledWith('message 1\n');
expect(mockStderr).toHaveBeenCalledWith('message 2\n');
});

test('handles nested corking correctly', async () => {
Expand All @@ -130,9 +136,9 @@ describe('logging', () => {
});

expect(mockStderr).toHaveBeenCalledTimes(3);
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('outer 1\n'));
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('inner\n'));
expect(mockStderr).toHaveBeenCalledWith(expect.stringContaining('outer 2\n'));
expect(mockStderr).toHaveBeenCalledWith('outer 1\n');
expect(mockStderr).toHaveBeenCalledWith('inner\n');
expect(mockStderr).toHaveBeenCalledWith('outer 2\n');
});
});

Expand All @@ -145,7 +151,7 @@ describe('logging', () => {
prefix: 'PREFIX',
});
expect(mockStderr).toHaveBeenCalledWith(
expect.stringMatching(/PREFIX \[\d{2}:\d{2}:\d{2}\] test message\n/),
expect.stringMatching(/^PREFIX \[\d{2}:\d{2}:\d{2}\] test message\n$/),
);
});
});
Expand Down
Loading

0 comments on commit d92d952

Please sign in to comment.