Skip to content

Commit 681d9b8

Browse files
committed
fix: improve maestro test output
1 parent 096785b commit 681d9b8

5 files changed

Lines changed: 123 additions & 26 deletions

File tree

src/__tests__/cli-network.test.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ test('test command prints suite summary and exits non-zero on failures', async (
106106
assert.equal(result.calls[0]?.meta?.requestProgress, 'replay-test');
107107
assert.match(result.stderr, /Running replay suite\.\.\./);
108108
assert.doesNotMatch(result.stdout, /PASS \/tmp\/01-pass\.ad/);
109-
assert.match(result.stdout, /FAIL \/tmp\/02-fail\.ad after 2 attempts \(5ms\)/);
109+
assert.match(result.stdout, /FAIL 02-fail\.ad after 2 attempts \(total 0\.005s\)/);
110110
assert.match(result.stdout, /Replay failed at step 1 \(open Demo\): boom/);
111111
assert.match(result.stdout, /artifacts: \/tmp\/test-artifacts\/02-fail/);
112112
assert.doesNotMatch(result.stdout, /SKIP \/tmp\/03-skip\.ad/);
113-
assert.match(result.stdout, /Test summary: 1 passed, 1 failed in 25ms/);
113+
assert.match(result.stdout, /Test summary: 1 passed, 1 failed in 0\.025s/);
114114
});
115115

116116
test('test command --verbose prints all test statuses', async () => {
@@ -120,8 +120,8 @@ test('test command --verbose prints all test statuses', async () => {
120120

121121
assert.equal(result.code, 1);
122122
assert.match(result.stderr, /Running replay suite\.\.\./);
123-
assert.match(result.stdout, /PASS \/tmp\/01-pass\.ad \(10ms\)/);
124-
assert.match(result.stdout, /SKIP \/tmp\/03-skip\.ad/);
123+
assert.match(result.stdout, /PASS 01-pass\.ad \(0\.01s\)/);
124+
assert.match(result.stdout, /SKIP 03-skip\.ad/);
125125
});
126126

127127
test('test command reports flaky passed-on-retry cases in the default summary', async () => {
@@ -138,20 +138,37 @@ test('test command reports flaky passed-on-retry cases in the default summary',
138138
failures: [],
139139
tests: [
140140
{
141-
file: '/tmp/01-flaky.ad',
141+
file: '/tmp/auth-flow.yml',
142142
session: 'default:test:suite:1',
143143
status: 'passed',
144-
durationMs: 10,
144+
durationMs: 112151,
145+
finalAttemptDurationMs: 17492,
145146
attempts: 2,
147+
attemptFailures: [
148+
{
149+
attempt: 1,
150+
message: 'Replay failed at step 3 (tapOn "Log in"): selector not found',
151+
durationMs: 94659,
152+
},
153+
],
146154
},
147155
],
148156
},
149157
}));
150158

151159
assert.equal(result.code, null);
152160
assert.match(result.stderr, /Running replay suite\.\.\./);
153-
assert.match(result.stdout, /FLAKY \/tmp\/01-flaky\.ad after 2 attempts \(10ms\)/);
154-
assert.match(result.stdout, /Test summary: 1 passed, 0 failed, 1 flaky in 25ms/);
161+
assert.doesNotMatch(result.stdout, /FLAKY/);
162+
assert.match(result.stdout, /Test summary: 1 passed, 0 failed, 1 flaky in 0\.025s/);
163+
assert.match(result.stdout, /Flaky tests:/);
164+
assert.match(
165+
result.stdout,
166+
/PASS auth-flow\.yml after 2 attempts \(passed attempt 17\.5s, total 112\.2s\)/,
167+
);
168+
assert.match(
169+
result.stdout,
170+
/attempt 1 failed \(94\.7s\): Replay failed at step 3 \(tapOn "Log in"\): selector not found/,
171+
);
155172
});
156173

157174
test('test --maestro forwards Maestro backend and platform for directory suites', async () => {

src/cli-test.ts

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,15 @@ function renderReplayTestSummary(
4040
for (const entry of data.failures) {
4141
renderFailedTestResult(entry);
4242
}
43-
for (const entry of flaky) {
44-
renderFlakyTestResult(entry);
45-
}
4643
}
4744

4845
const durationMs = typeof data.durationMs === 'number' ? data.durationMs : undefined;
4946
const flakySuffix = flaky.length > 0 ? `, ${flaky.length} flaky` : '';
47+
const durationSuffix = durationMs !== undefined ? ` in ${formatDurationSeconds(durationMs)}` : '';
5048
process.stdout.write(
51-
`Test summary: ${data.passed} passed, ${data.failed} failed${flakySuffix}${durationMs !== undefined ? ` in ${durationMs}ms` : ''}\n`,
49+
`Test summary: ${data.passed} passed, ${data.failed} failed${flakySuffix}${durationSuffix}\n`,
5250
);
51+
renderFlakyTestSummary(flaky);
5352
return getReplayTestExitCode(data);
5453
}
5554

@@ -59,11 +58,10 @@ function renderVerboseTestResult(result: ReplaySuiteTestResult): void {
5958
return;
6059
}
6160

62-
const prefix = replayResultPrefix(result);
63-
const attemptSuffix =
64-
'attempts' in result && result.attempts > 1 ? ` after ${result.attempts} attempts` : '';
65-
const durationSuffix = result.durationMs > 0 ? ` (${result.durationMs}ms)` : '';
66-
process.stdout.write(`${prefix} ${result.file}${attemptSuffix}${durationSuffix}\n`);
61+
const durationSuffix = formatReplayTestDurationSuffix(result);
62+
process.stdout.write(
63+
`${replayResultPrefix(result)} ${replayTestDisplayName(result)}${durationSuffix}\n`,
64+
);
6765
if (result.status === 'skipped') {
6866
process.stdout.write(` ${result.message ?? 'skipped'}\n`);
6967
}
@@ -73,16 +71,16 @@ function renderFailedTestResult(
7371
result: Extract<ReplaySuiteTestResult, { status: 'failed' }>,
7472
): void {
7573
const attemptSuffix = result.attempts > 1 ? ` after ${result.attempts} attempts` : '';
76-
const durationSuffix = result.durationMs > 0 ? ` (${result.durationMs}ms)` : '';
77-
process.stdout.write(`FAIL ${result.file}${attemptSuffix}${durationSuffix}\n`);
74+
const durationSuffix = formatReplayTestDurationSuffix(result);
75+
process.stdout.write(`FAIL ${replayTestDisplayName(result)}${attemptSuffix}${durationSuffix}\n`);
7876
process.stdout.write(` ${result.error?.message ?? 'Unknown test failure'}\n`);
7977
for (const line of replayFailureConsoleLines(result)) {
8078
process.stdout.write(` ${line}\n`);
8179
}
8280
}
8381

8482
function replayResultPrefix(result: ReplaySuiteTestResult): string {
85-
if (result.status === 'passed') return result.attempts > 1 ? 'FLAKY' : 'PASS';
83+
if (result.status === 'passed') return 'PASS';
8684
if (result.status === 'skipped') return 'SKIP';
8785
return 'INFO';
8886
}
@@ -98,17 +96,64 @@ function replayFailureConsoleLines(
9896
].filter(Boolean);
9997
}
10098

101-
function renderFlakyTestResult(result: Extract<ReplaySuiteTestResult, { status: 'passed' }>): void {
102-
const durationSuffix = result.durationMs > 0 ? ` (${result.durationMs}ms)` : '';
103-
process.stdout.write(`FLAKY ${result.file} after ${result.attempts} attempts${durationSuffix}\n`);
104-
}
105-
10699
function isFlakyReplayTestResult(
107100
result: ReplaySuiteTestResult,
108101
): result is Extract<ReplaySuiteTestResult, { status: 'passed' }> {
109102
return result.status === 'passed' && result.attempts > 1;
110103
}
111104

105+
function renderFlakyTestSummary(
106+
results: Array<Extract<ReplaySuiteTestResult, { status: 'passed' }>>,
107+
): void {
108+
if (results.length === 0) return;
109+
process.stdout.write('Flaky tests:\n');
110+
for (const result of results) {
111+
process.stdout.write(
112+
` PASS ${replayTestDisplayName(result)} after ${result.attempts} attempts${formatFlakyReplayDurationSuffix(result)}\n`,
113+
);
114+
for (const failure of result.attemptFailures ?? []) {
115+
const attemptDuration =
116+
typeof failure.durationMs === 'number'
117+
? ` (${formatDurationSeconds(failure.durationMs)})`
118+
: '';
119+
process.stdout.write(
120+
` attempt ${failure.attempt} failed${attemptDuration}: ${failure.message}\n`,
121+
);
122+
}
123+
}
124+
}
125+
126+
function replayTestDisplayName(result: ReplaySuiteTestResult): string {
127+
return path.basename(result.file);
128+
}
129+
130+
function formatReplayTestDurationSuffix(result: ReplaySuiteTestResult): string {
131+
if (result.status === 'passed' && result.attempts > 1) {
132+
return formatFlakyReplayDurationSuffix(result);
133+
}
134+
if (result.status === 'failed' && result.attempts > 1 && result.durationMs > 0) {
135+
return ` (total ${formatDurationSeconds(result.durationMs)})`;
136+
}
137+
138+
const durationMs =
139+
result.status === 'passed' && typeof result.finalAttemptDurationMs === 'number'
140+
? result.finalAttemptDurationMs
141+
: result.durationMs;
142+
return durationMs > 0 ? ` (${formatDurationSeconds(durationMs)})` : '';
143+
}
144+
145+
function formatFlakyReplayDurationSuffix(
146+
result: Extract<ReplaySuiteTestResult, { status: 'passed' }>,
147+
): string {
148+
const timings = [
149+
typeof result.finalAttemptDurationMs === 'number'
150+
? `passed attempt ${formatDurationSeconds(result.finalAttemptDurationMs)}`
151+
: '',
152+
result.durationMs > 0 ? `total ${formatDurationSeconds(result.durationMs)}` : '',
153+
].filter(Boolean);
154+
return timings.length > 0 ? ` (${timings.join(', ')})` : '';
155+
}
156+
112157
function getReplayTestExitCode(data: ReplaySuiteResult): number {
113158
return data.failed > 0 ? 1 : 0;
114159
}
@@ -144,7 +189,7 @@ function buildReplayJunitXml(suite: ReplaySuiteResult): string {
144189
}
145190

146191
function renderJUnitTestCase(test: ReplaySuiteTestResult): string[] {
147-
const name = xmlEscape(path.basename(test.file));
192+
const name = xmlEscape(replayTestDisplayName(test));
148193
const className = xmlEscape(
149194
path.dirname(test.file) === '.' ? test.file : path.dirname(test.file),
150195
);
@@ -239,6 +284,13 @@ function formatJUnitSeconds(durationMs: number): string {
239284
return (Math.max(0, durationMs) / 1000).toFixed(3);
240285
}
241286

287+
function formatDurationSeconds(durationMs: number): string {
288+
const seconds = Math.max(0, durationMs) / 1000;
289+
if (seconds >= 10) return `${seconds.toFixed(1)}s`;
290+
if (seconds >= 1) return `${seconds.toFixed(2)}s`;
291+
return `${seconds.toFixed(3).replace(/0+$/, '').replace(/\.$/, '')}s`;
292+
}
293+
242294
function xmlEscape(value: string): string {
243295
return value
244296
.replaceAll('&', '&amp;')

src/daemon/handlers/__tests__/session-test-suite.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ test('test emits progress when attempts retry and pass', async () => {
130130

131131
const data = expectOkData(response);
132132
expect(data.passed).toBe(1);
133+
expect((data.tests as Array<Record<string, unknown>>)[0]?.attemptFailures).toEqual([
134+
{
135+
attempt: 1,
136+
message: 'Replay failed at step 1 (open "Demo"): first attempt failed',
137+
durationMs: expect.any(Number),
138+
},
139+
]);
133140
expect(events.map((event) => event.status)).toEqual(['fail', 'pass']);
134141
expect(events[0]).toMatchObject({
135142
type: 'replay-test',

src/daemon/handlers/session-test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,14 @@ async function runReplayTestCase(
150150
let finalResponse: DaemonResponse | undefined;
151151
let finalSessionName = '';
152152
let attempts = 0;
153+
let finalAttemptDurationMs = 0;
154+
const attemptFailures: NonNullable<
155+
Extract<ReplaySuiteTestResult, { status: 'passed' }>['attemptFailures']
156+
> = [];
153157

154158
for (let attemptIndex = 0; attemptIndex <= retries; attemptIndex += 1) {
155159
attempts = attemptIndex + 1;
160+
const attemptStartedAt = Date.now();
156161
const testSessionName = buildReplayTestSessionName(
157162
sessionName,
158163
suiteInvocationId,
@@ -181,6 +186,7 @@ async function runReplayTestCase(
181186
runReplay,
182187
cleanupSession,
183188
});
189+
finalAttemptDurationMs = Date.now() - attemptStartedAt;
184190
materializeReplayTestAttemptArtifacts({
185191
response,
186192
filePath: entry.path,
@@ -192,6 +198,11 @@ async function runReplayTestCase(
192198
finalResponse = response;
193199
finalSessionName = testSessionName;
194200
if (response.ok) break;
201+
attemptFailures.push({
202+
attempt: attempts,
203+
message: response.error.message,
204+
durationMs: finalAttemptDurationMs,
205+
});
195206
if (isReplayInfrastructureFailure(response)) break;
196207
if (attemptIndex >= retries) break;
197208
emitRequestProgress({
@@ -225,10 +236,12 @@ async function runReplayTestCase(
225236
session: finalSessionName,
226237
status: 'passed',
227238
durationMs,
239+
finalAttemptDurationMs,
228240
attempts,
229241
artifactsDir: testArtifactsDir,
230242
replayed: typeof finalResponse.data?.replayed === 'number' ? finalResponse.data.replayed : 0,
231243
healed: typeof finalResponse.data?.healed === 'number' ? finalResponse.data.healed : 0,
244+
...(attemptFailures.length > 0 ? { attemptFailures } : {}),
232245
};
233246
}
234247

src/daemon/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ export type ReplaySuiteTestPassed = {
4141
session: string;
4242
status: 'passed';
4343
durationMs: number;
44+
finalAttemptDurationMs?: number;
4445
attempts: number;
4546
artifactsDir?: string;
4647
replayed: number;
4748
healed: number;
49+
attemptFailures?: ReplaySuiteAttemptFailure[];
4850
};
4951

5052
export type ReplaySuiteTestFailed = {
@@ -72,6 +74,12 @@ export type ReplaySuiteTestSkipped = {
7274
message: string;
7375
};
7476

77+
export type ReplaySuiteAttemptFailure = {
78+
attempt: number;
79+
message: string;
80+
durationMs?: number;
81+
};
82+
7583
export type ReplaySuiteTestResult =
7684
| ReplaySuiteTestPassed
7785
| ReplaySuiteTestFailed

0 commit comments

Comments
 (0)