Skip to content
Merged
Changes from 2 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
82 changes: 54 additions & 28 deletions dev-packages/e2e-tests/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -373,42 +373,68 @@

const results = [];

// Retry only on the known Cirrus Labs Tart VM flake where the app fails
// to reach "E2E Tests Ready" after a fresh launchApp. Maestro's exit code
// is a flat 0/1 (see TestCommand.kt), so the pattern must be detected in
// stdout. Real assertion failures elsewhere (e.g. assertEventIdVisible)
// are surfaced immediately.
const READY_ASSERTION_FLAKE = 'Assert that "E2E Tests Ready" is visible... FAILED';
const MAX_ATTEMPTS = 2;

// Run each flow in its own process to prevent crash cascade โ€”
// when crash.yml kills the app, a shared Maestro session would fail
// all subsequent flows.
console.log('Waiting for flows to complete...');
for (const flow of flowFiles) {
const flowPath = path.join('maestro', flow);
const startTime = Date.now();
try {
execFileSync('maestro', [
'test',
flowPath,
'--env', `APP_ID=${appId}`,
'--env', `SENTRY_AUTH_TOKEN=${sentryAuthToken}`,
'--debug-output', 'maestro-logs',
'--flatten-debug-output',
], {
stdio: 'pipe',
cwd: e2eDir,
});
const elapsed = Math.round((Date.now() - startTime) / 1000);
const name = flow.replace('.yml', '');
results.push({ name, passed: true, elapsed });
console.log(`[Passed] ${name} (${elapsed}s)`);
} catch (error) {
const elapsed = Math.round((Date.now() - startTime) / 1000);
const name = flow.replace('.yml', '');
const output = (error.stdout?.toString() || '') + (error.stderr?.toString() || '');
const detail = output.split('\n').find(l =>
l.includes('App crashed') || l.includes('Element not found') || l.includes('FAILED')) || '';
results.push({ name, passed: false, elapsed, detail });
console.log(`[Failed] ${name} (${elapsed}s)${detail ? ` (${detail.trim()})` : ''}`);
// Dump Maestro output for failed flows to aid debugging
if (output) {
console.log(`\n--- ${name} output ---\n${output.trim()}\n--- end ${name} output ---\n`);
const name = flow.replace('.yml', '');
let lastElapsed = 0;
let lastOutput = '';
let lastDetail = '';
let passed = false;

for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const startTime = Date.now();
try {
execFileSync('maestro', [
'test',
flowPath,
'--env', `APP_ID=${appId}`,
'--env', `SENTRY_AUTH_TOKEN=${sentryAuthToken}`,
'--debug-output', 'maestro-logs',
'--flatten-debug-output',
], {
stdio: 'pipe',
cwd: e2eDir,
});
lastElapsed = Math.round((Date.now() - startTime) / 1000);
passed = true;
const suffix = attempt > 1 ? ` (attempt ${attempt}/${MAX_ATTEMPTS})` : '';
console.log(`[Passed] ${name} (${lastElapsed}s)${suffix}`);
break;
} catch (error) {
lastElapsed = Math.round((Date.now() - startTime) / 1000);
lastOutput = (error.stdout?.toString() || '') + (error.stderr?.toString() || '');
lastDetail = lastOutput.split('\n').find(l =>
l.includes('App crashed') || l.includes('Element not found') || l.includes('FAILED')) || '';

const isReadyFlake = lastOutput.includes(READY_ASSERTION_FLAKE);
const canRetry = isReadyFlake && attempt < MAX_ATTEMPTS;

if (canRetry) {
console.log(`[Flaky] ${name} (${lastElapsed}s)${lastDetail ? ` (${lastDetail.trim()})` : ''} โ€” retrying (${attempt + 1}/${MAX_ATTEMPTS})`);
// Brief pause to let the simulator/driver settle before retrying
execFileSync('sleep', ['5']);
Comment thread
lucas-zimerman marked this conversation as resolved.
} else {
console.log(`[Failed] ${name} (${lastElapsed}s)${lastDetail ? ` (${lastDetail.trim()})` : ''}`);
if (lastOutput) {
console.log(`\n--- ${name} output ---\n${lastOutput.trim()}\n--- end ${name} output ---\n`);

Check warning on line 431 in dev-packages/e2e-tests/cli.mjs

View check run for this annotation

@sentry/warden / warden: find-bugs

Non-flake failure on attempt 1 still retries because else branch lacks break

When a flow fails on the first attempt for a real (non-flake) reason, the `else` branch logs `[Failed]` but does not `break`, so the `for` loop increments to attempt 2 and re-runs the flow anyway. This wastes CI time and can mask a regression: if the second run passes, `passed` becomes true and the flow is reported `[Passed]`, contradicting the PR's stated intent that real assertion failures fail immediately.
}

Check warning on line 432 in dev-packages/e2e-tests/cli.mjs

View check run for this annotation

@sentry/warden / warden: code-review

Non-flaky failures are retried instead of failing immediately

In the flow retry loop, the `else` branch that handles non-flaky failures logs `[Failed]` but does not `break` out of the `for (attempt...)` loop. Because `MAX_ATTEMPTS = 2`, a real (non-ready-flake) failure at attempt 1 still falls through to attempt 2 and re-runs `maestro test`. If the second run happens to pass, `passed` becomes `true` and the flow is reported as `[Passed] ... (attempt 2/2)`. This contradicts the PR's stated intent that every non-flake failure 'still fails immediately, so regressions are not masked,' and it can hide flaky-passing regressions the retry logic was meant to preserve.
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated
}

results.push({ name, passed, elapsed: lastElapsed, detail: passed ? '' : lastDetail });
}

const failedFlows = results.filter(r => !r.passed).map(r => r.name);
Expand Down
Loading