Skip to content

Commit ec93ebd

Browse files
authored
fix(toolkit-lib): deployment total time includes stray output (#238)
Fixes an extra `Deployment <number>` at the end of the "Total Time" message reporting the total deployment time. Adds missing test cases for the `MessageSpan` feature (GenAI supported). --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 53ea256 commit ec93ebd

File tree

2 files changed

+249
-18
lines changed

2 files changed

+249
-18
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/span.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,24 @@ export class SpanMaker<S extends object, E extends SpanEnd> {
106106
*/
107107
public async begin(payload: VoidWhenEmpty<S>): Promise<IMessageSpan<E>>;
108108
public async begin(message: string, payload: S): Promise<IMessageSpan<E>>;
109-
public async begin(first: any, second?: S): Promise<IMessageSpan<E>> {
109+
public async begin(a: any, b?: S): Promise<IMessageSpan<E>> {
110110
const spanId = uuid.v4();
111111
const startTime = new Date().getTime();
112112

113113
const notify = (msg: ActionLessMessage<unknown>): Promise<void> => {
114114
return this.ioHelper.notify(withSpanId(spanId, msg));
115115
};
116116

117-
const startMsg = second ? first : 'Starting %s ...';
118-
const startPayload = second ?? first;
117+
const startInput = parseArgs<S>(a, b);
118+
const startMsg = startInput.message ?? `Starting ${this.definition.name} ...`;
119+
const startPayload = startInput.payload;
119120

120121
await notify(this.definition.start.msg(
121-
util.format(startMsg, this.definition.name),
122+
startMsg,
122123
startPayload,
123124
));
124125

125-
const timingMsg = '\n✨ %s time: %ds\n';
126+
const timingMsgTemplate = '\n✨ %s time: %ds\n';
126127
const time = () => {
127128
const elapsedTime = new Date().getTime() - startTime;
128129
return {
@@ -132,13 +133,7 @@ export class SpanMaker<S extends object, E extends SpanEnd> {
132133
};
133134

134135
return {
135-
elapsedTime: async (msg?: ActionLessMessage<object>): Promise<ElapsedTime> => {
136-
if (msg) {
137-
await notify({
138-
...msg,
139-
data: msg.data,
140-
});
141-
}
136+
elapsedTime: async (): Promise<ElapsedTime> => {
142137
return time();
143138
},
144139

@@ -148,20 +143,22 @@ export class SpanMaker<S extends object, E extends SpanEnd> {
148143

149144
timing: async(maker: IoMessageMaker<Duration>, message?: string): Promise<ElapsedTime> => {
150145
const duration = time();
151-
const endMsg = message ? message : timingMsg;
152-
await notify(maker.msg(util.format(endMsg, this.definition.name, duration.asSec), {
146+
const timingMsg = message ? message : util.format(timingMsgTemplate, this.definition.name, duration.asSec);
147+
await notify(maker.msg(timingMsg, {
153148
duration: duration.asMs,
154149
}));
155150
return duration;
156151
},
157152

158-
end: async (a: any, b?: ForceEmpty<Optional<E, keyof SpanEnd>>): Promise<ElapsedTime> => {
153+
end: async (x: any, y?: ForceEmpty<Optional<E, keyof SpanEnd>>): Promise<ElapsedTime> => {
159154
const duration = time();
160-
const endMsg = b ? a : timingMsg;
161-
const endPayload = b ?? a;
155+
156+
const endInput = parseArgs<ForceEmpty<Optional<E, keyof SpanEnd>>>(x, y);
157+
const endMsg = endInput.message ?? util.format(timingMsgTemplate, this.definition.name, duration.asSec);
158+
const endPayload = endInput.payload;
162159

163160
await notify(this.definition.end.msg(
164-
util.format(endMsg, this.definition.name, duration.asSec), {
161+
endMsg, {
165162
duration: duration.asMs,
166163
...endPayload,
167164
} as E));
@@ -172,6 +169,22 @@ export class SpanMaker<S extends object, E extends SpanEnd> {
172169
}
173170
}
174171

172+
function parseArgs<S extends object>(first: any, second?: S): { message: string | undefined; payload: S } {
173+
const firstIsMessage = typeof first === 'string';
174+
175+
// When the first argument is a string or we have a second argument, then the first arg is the message
176+
const message = (firstIsMessage || second) ? first : undefined;
177+
178+
// When the first argument is a string or we have a second argument,
179+
// then the second arg is the payload, otherwise the first arg is the payload
180+
const payload = (firstIsMessage || second) ? second : first;
181+
182+
return {
183+
message,
184+
payload,
185+
};
186+
}
187+
175188
function withSpanId(span: string, message: ActionLessMessage<unknown>): ActionLessMessage<unknown> {
176189
return {
177190
...message,
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/* eslint-disable @typescript-eslint/unbound-method */
2+
import type { IoHelper, SpanDefinition } from '../../../src/api/io/private';
3+
import { SpanMaker } from '../../../src/api/io/private';
4+
import * as maker from '../../../src/api/io/private/message-maker';
5+
6+
describe('SpanMaker', () => {
7+
let ioHelper: jest.Mocked<IoHelper>;
8+
9+
beforeEach(() => {
10+
ioHelper = {
11+
notify: jest.fn(),
12+
} as any;
13+
});
14+
15+
test('begin creates span with unique id and sends start message', async () => {
16+
// GIVEN
17+
const definition: SpanDefinition<{}, { duration: number }> = {
18+
name: 'Test Span',
19+
start: maker.info<{}>({
20+
code: 'CDK_TOOLKIT_I1234',
21+
description: 'test',
22+
interface: 'stuff',
23+
}),
24+
end: maker.info<{ duration: number }>({
25+
code: 'CDK_TOOLKIT_I1234',
26+
description: 'test',
27+
interface: 'stuff',
28+
}),
29+
};
30+
const spanMaker = new SpanMaker(ioHelper, definition);
31+
32+
// WHEN
33+
await spanMaker.begin('Test span', {});
34+
35+
// THEN
36+
expect(ioHelper.notify).toHaveBeenCalledTimes(1);
37+
const notifyCall = ioHelper.notify.mock.calls[0][0];
38+
expect(notifyCall.message).toBe('Test span');
39+
expect(notifyCall.span).toBeDefined(); // UUID should be set
40+
});
41+
42+
test('end returns elapsed time and sends end message', async () => {
43+
// GIVEN
44+
const definition: SpanDefinition<{}, { duration: number }> = {
45+
name: 'Test Span',
46+
start: maker.info<{}>({
47+
code: 'CDK_TOOLKIT_I1234',
48+
description: 'Starting span',
49+
interface: 'stuff',
50+
}),
51+
end: maker.info<{ duration: number }>({
52+
code: 'CDK_TOOLKIT_I1234',
53+
description: 'Ending span',
54+
interface: 'stuff',
55+
}),
56+
};
57+
const spanMaker = new SpanMaker(ioHelper, definition);
58+
59+
// WHEN
60+
const messageSpan = await spanMaker.begin('Test span', {});
61+
const result = await messageSpan.end();
62+
63+
// THEN
64+
expect(result.asMs).toBeGreaterThanOrEqual(0);
65+
expect(ioHelper.notify).toHaveBeenCalledTimes(2);
66+
const endCall = ioHelper.notify.mock.calls[1][0] as any;
67+
expect(endCall.message).toContain('Test Span time');
68+
expect(endCall.span).toBeDefined();
69+
expect(endCall.data.duration).toBeGreaterThanOrEqual(0);
70+
});
71+
72+
test('intermediate messages are sent with same span id', async () => {
73+
// GIVEN
74+
const definition: SpanDefinition<{}, { duration: number }> = {
75+
name: 'Test Span',
76+
start: maker.info<{}>({
77+
code: 'CDK_TOOLKIT_I1234',
78+
description: 'Starting span',
79+
interface: 'stuff',
80+
}),
81+
end: maker.info<{ duration: number }>({
82+
code: 'CDK_TOOLKIT_I1234',
83+
description: 'Ending span',
84+
interface: 'stuff',
85+
}),
86+
};
87+
const spanMaker = new SpanMaker(ioHelper, definition);
88+
89+
// WHEN
90+
const messageSpan = await spanMaker.begin('Test span', {});
91+
await messageSpan.notify({
92+
message: 'Intermediate message',
93+
code: 'CDK_TOOLKIT_I1234',
94+
time: new Date(),
95+
level: 'error',
96+
data: undefined,
97+
});
98+
await messageSpan.end();
99+
100+
// THEN
101+
expect(ioHelper.notify).toHaveBeenCalledTimes(3);
102+
const spanId = ioHelper.notify.mock.calls[0][0].span;
103+
expect(ioHelper.notify.mock.calls[1][0].span).toBe(spanId);
104+
expect(ioHelper.notify.mock.calls[2][0].span).toBe(spanId);
105+
});
106+
107+
test('end with payload overrides default elapsed time payload', async () => {
108+
// GIVEN
109+
const definition: SpanDefinition<{}, { customField: string; duration: number }> = {
110+
name: 'Test Span',
111+
start: maker.info<{}>({
112+
code: 'CDK_TOOLKIT_I1234',
113+
description: 'Starting span',
114+
interface: 'stuff',
115+
}),
116+
end: maker.info<{ customField: string; duration: number }>({
117+
code: 'CDK_TOOLKIT_I1234',
118+
description: 'Ending span',
119+
interface: 'stuff',
120+
}),
121+
};
122+
const spanMaker = new SpanMaker(ioHelper, definition);
123+
124+
// WHEN
125+
const messageSpan = await spanMaker.begin('Test span', {});
126+
const result = await messageSpan.end({ customField: 'test value' });
127+
128+
// THEN
129+
expect(result).toEqual({ asMs: expect.any(Number), asSec: expect.any(Number) });
130+
const endCall = ioHelper.notify.mock.calls[1][0];
131+
expect(endCall.data).toEqual(expect.objectContaining({ customField: 'test value' }));
132+
});
133+
134+
test('begin with payload includes payload in start message', async () => {
135+
// GIVEN
136+
const definition: SpanDefinition<{ startField: string }, { duration: number }> = {
137+
name: 'Test Span',
138+
start: maker.info<{ startField: string }>({
139+
code: 'CDK_TOOLKIT_I1234',
140+
description: 'Starting span',
141+
interface: 'stuff',
142+
}),
143+
end: maker.info<{ duration: number }>({
144+
code: 'CDK_TOOLKIT_I1234',
145+
description: 'Ending span',
146+
interface: 'stuff',
147+
}),
148+
};
149+
const spanMaker = new SpanMaker(ioHelper, definition);
150+
151+
// WHEN
152+
await spanMaker.begin('Test span', { startField: 'test value' });
153+
154+
// THEN
155+
const startCall = ioHelper.notify.mock.calls[0][0];
156+
expect(startCall.data).toEqual({ startField: 'test value' });
157+
});
158+
159+
test('endWithMessage allows custom end message', async () => {
160+
// GIVEN
161+
const definition: SpanDefinition<{}, { duration: number }> = {
162+
name: 'Test Span',
163+
start: maker.info<{}>({
164+
code: 'CDK_TOOLKIT_I1234',
165+
description: 'Starting span',
166+
interface: 'stuff',
167+
}),
168+
end: maker.info<{ duration: number }>({
169+
code: 'CDK_TOOLKIT_I1234',
170+
description: 'Ending span',
171+
interface: 'stuff',
172+
}),
173+
};
174+
const spanMaker = new SpanMaker(ioHelper, definition);
175+
176+
// WHEN
177+
const messageSpan =await spanMaker.begin('Test span', {});
178+
await messageSpan.end('Custom end message');
179+
180+
// THEN
181+
const endCall = ioHelper.notify.mock.calls[1][0];
182+
expect(endCall.message).toBe('Custom end message');
183+
});
184+
185+
test('timing sends timing message with elapsed time', async () => {
186+
// GIVEN
187+
const definition: SpanDefinition<{}, { duration: number }> = {
188+
name: 'Test Span',
189+
start: maker.info<{}>({
190+
code: 'CDK_TOOLKIT_I1234',
191+
description: 'Starting span',
192+
interface: 'stuff',
193+
}),
194+
end: maker.info<{ duration: number }>({
195+
code: 'CDK_TOOLKIT_I1234',
196+
description: 'Ending span',
197+
interface: 'stuff',
198+
}),
199+
};
200+
const spanMaker = new SpanMaker(ioHelper, definition);
201+
202+
// WHEN
203+
const messageSpan = await spanMaker.begin('Test span', {});
204+
const elapsedTime = await messageSpan.timing(maker.info({
205+
code: 'CDK_TOOLKIT_I1234',
206+
description: 'Timing message',
207+
interface: 'stuff',
208+
}), 'Custom timing message');
209+
210+
// THEN
211+
expect(elapsedTime.asMs).toBeGreaterThanOrEqual(0);
212+
expect(ioHelper.notify).toHaveBeenCalledTimes(2);
213+
const timingCall = ioHelper.notify.mock.calls[1][0] as any;
214+
expect(timingCall.message).toBe('Custom timing message');
215+
expect(timingCall.span).toBeDefined();
216+
expect(timingCall.data.duration).toBeGreaterThanOrEqual(0);
217+
});
218+
});

0 commit comments

Comments
 (0)