Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { integTest, withDefaultFixture } from '../../../lib';

integTest(
'cdk ls prints only the stack listing, no status lines',
withDefaultFixture(async (fixture) => {
// `cdk ls` output should be the stack listing only, never status lines like the synth time
// or "Including dependency stacks". `*list-stacks` is selected because it pulls in a dependency.
for (const args of [['ls'], ['ls', `${fixture.stackNamePrefix}-list-stacks`]]) {
const output = await fixture.cdk(args, {
verbose: false,
modEnv: { CI: 'true' },
});

// no status lines
expect(output).not.toContain('Synthesis time');
expect(output).not.toContain('Including dependency stacks');

// every line should be a stack
const lines = output.trim().split('\n').filter(line => line.length > 0);
expect(lines.length).toBeGreaterThan(0);
for (const line of lines) {
expect(line).toContain(fixture.stackNamePrefix);
}
}
}),
);
20 changes: 11 additions & 9 deletions packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,17 +966,19 @@ export class CdkToolkit {
): Promise<number> {
this.ioHost.rewriteOnce(IO.CDK_TOOLKIT_I2901, (msg) => formatStackList(msg.data.stacks, options));

// With `--json`, stdout must stay machine-parsable, so suppress the synth-time line (I1000).
if (options.json) {
this.ioHost.once(IO.CDK_TOOLKIT_I1000, () => ({ preventDefault: true }));
// Only the listing should reach stdout; drop info status lines (synth time, dependency notes).
const previousLevel = this.ioHost.logLevel;
this.ioHost.logLevel = 'warn';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem like the right approach

try {
await this.toolkit.list(this.props.cloudExecutable, {
stacks: selectors.length > 0
? { patterns: selectors, strategy: StackSelectionStrategy.PATTERN_MATCH, expand: ExpandStackSelection.UPSTREAM }
: undefined,
});
} finally {
this.ioHost.logLevel = previousLevel;
}

await this.toolkit.list(this.props.cloudExecutable, {
stacks: selectors.length > 0
? { patterns: selectors, strategy: StackSelectionStrategy.PATTERN_MATCH, expand: ExpandStackSelection.UPSTREAM }
: undefined,
});

return 0; // exit-code
}

Expand Down
43 changes: 27 additions & 16 deletions packages/aws-cdk/test/cli/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,33 +216,44 @@ describe('list', () => {
]);
});

test('with --json, suppresses the synthesis-time line so stdout stays machine-parsable', async () => {
// `cdk ls --json` stdout is a machine-readable contract; the "Synthesis time" line
// (CDK_TOOLKIT_I1000) must not be written (in CI mode non-error output goes to stdout).
test('raises the log level to warn while listing, so info status lines are dropped', async () => {
// The "Synthesis time" line and "Including dependency stacks" line are info-level; while
// listing, the log level is raised to warn so they never reach stdout next to the listing.
const toolkit = defaultToolkitSetup();
const onceSpy = jest.spyOn(ioHost, 'once');
let levelDuringList: string | undefined;
const listSpy = jest.spyOn(Toolkit.prototype, 'list').mockImplementation(async () => {
levelDuringList = ioHost.logLevel;
return [];
});

// WHEN
await toolkit.list([], { json: true });
await toolkit.list([]);

// THEN - a one-shot suppressor for I1000 was registered that prevents default handling.
const i1000Call = onceSpy.mock.calls.find(([code]) => (code as any)?.code === 'CDK_TOOLKIT_I1000');
expect(i1000Call).toBeDefined();
const listener = i1000Call![1] as (msg: any) => any;
expect(listener({ code: 'CDK_TOOLKIT_I1000' })).toEqual({ preventDefault: true });
// THEN - info-level messages are filtered out during the listing
expect(levelDuringList).toEqual('warn');
listSpy.mockRestore();
});

test('without --json, does not suppress the synthesis-time line', async () => {
// Plain `cdk ls` is not a machine-readable contract, so the line is left alone.
test('restores the previous log level after listing', async () => {
const toolkit = defaultToolkitSetup();
const onceSpy = jest.spyOn(ioHost, 'once');
ioHost.logLevel = 'info';

// WHEN
await toolkit.list([]);

// THEN
const i1000Call = onceSpy.mock.calls.find(([code]) => (code as any)?.code === 'CDK_TOOLKIT_I1000');
expect(i1000Call).toBeUndefined();
// THEN - the temporary raise does not leak into later output (e.g. notices)
expect(ioHost.logLevel).toEqual('info');
});

test('restores the previous log level even if listing throws', async () => {
const toolkit = defaultToolkitSetup();
ioHost.logLevel = 'info';
const listSpy = jest.spyOn(Toolkit.prototype, 'list').mockRejectedValue(new Error('boom'));

// WHEN / THEN
await expect(toolkit.list([])).rejects.toThrow('boom');
expect(ioHost.logLevel).toEqual('info');
listSpy.mockRestore();
});
});

Expand Down
Loading