Skip to content

Commit 1ce6217

Browse files
committed
Retry only failed Android E2E flows
1 parent 526016d commit 1ce6217

6 files changed

Lines changed: 317 additions & 67 deletions

File tree

.github/actions/maestro-android/action.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ inputs:
2626
required: false
2727
default: x86
2828
description: The architecture of the emulator to run
29+
test-state-path:
30+
required: false
31+
default: /tmp/maestro-android-state/results.json
32+
description: The path used to persist per-flow test results between retries
33+
test-state-artifact-name:
34+
required: true
35+
description: The artifact used to pass per-flow test results to retry jobs
2936

3037
runs:
3138
using: composite
@@ -64,7 +71,7 @@ runs:
6471
cores: '4'
6572
disable-animations: false
6673
avd-name: e2e_emulator
67-
script: node .github/workflow-scripts/maestro-android.js ${{ inputs.app-path }} ${{ inputs.app-id }} ${{ inputs.maestro-flow }} ${{ inputs.flavor }} ${{ inputs.working-directory }}
74+
script: node .github/workflow-scripts/maestro-android.js ${{ inputs.app-path }} ${{ inputs.app-id }} ${{ inputs.maestro-flow }} ${{ inputs.flavor }} ${{ inputs.working-directory }} ${{ inputs.test-state-path }}
6875
- name: Normalize APP_ID
6976
id: normalize-app-id
7077
shell: bash
@@ -81,6 +88,14 @@ runs:
8188
path: |
8289
report.xml
8390
screen.mp4
91+
- name: Store per-flow test state
92+
uses: actions/upload-artifact@v6
93+
if: always()
94+
with:
95+
name: ${{ inputs.test-state-artifact-name }}
96+
overwrite: true
97+
if-no-files-found: warn
98+
path: ${{ inputs.test-state-path }}
8499
- name: Store Logs
85100
if: steps.run-tests.outcome == 'failure'
86101
uses: actions/upload-artifact@v6
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const fs = require('fs');
11+
const os = require('os');
12+
const path = require('path');
13+
14+
const {
15+
collectFlows,
16+
executeFlowSuite,
17+
loadState,
18+
} = require('../maestro-android');
19+
20+
describe('Maestro Android runner', () => {
21+
let temporaryDirectory;
22+
23+
beforeEach(() => {
24+
temporaryDirectory = fs.mkdtempSync(
25+
path.join(os.tmpdir(), 'maestro-android-test-'),
26+
);
27+
});
28+
29+
afterEach(() => {
30+
fs.rmSync(temporaryDirectory, {recursive: true, force: true});
31+
});
32+
33+
it('collects flows recursively in a stable order', () => {
34+
const nestedDirectory = path.join(temporaryDirectory, 'nested');
35+
fs.mkdirSync(nestedDirectory);
36+
fs.writeFileSync(path.join(temporaryDirectory, 'second.yaml'), 'appId: x');
37+
fs.writeFileSync(path.join(temporaryDirectory, 'image.png'), 'not a flow');
38+
fs.writeFileSync(path.join(nestedDirectory, 'first.yml'), 'appId: x');
39+
40+
expect(collectFlows(temporaryDirectory)).toEqual([
41+
path.join(nestedDirectory, 'first.yml'),
42+
path.join(temporaryDirectory, 'second.yaml'),
43+
]);
44+
});
45+
46+
it('runs every flow and retries only flows that have not passed', () => {
47+
const flows = ['first.yml', 'second.yml', 'third.yml'].map(file =>
48+
path.join(temporaryDirectory, file),
49+
);
50+
const statePath = path.join(temporaryDirectory, 'state', 'results.json');
51+
const firstAttempt = jest.fn(flow => {
52+
if (flow.endsWith('second.yml')) {
53+
throw new Error('failed assertion');
54+
}
55+
});
56+
57+
expect(() =>
58+
executeFlowSuite({
59+
flows,
60+
appId: 'com.example',
61+
state: loadState(statePath),
62+
statePath,
63+
executeFlow: firstAttempt,
64+
}),
65+
).toThrow('1 Maestro flow(s) failed');
66+
expect(firstAttempt).toHaveBeenCalledTimes(3);
67+
68+
const retry = jest.fn();
69+
executeFlowSuite({
70+
flows,
71+
appId: 'com.example',
72+
state: loadState(statePath),
73+
statePath,
74+
executeFlow: retry,
75+
});
76+
77+
expect(retry).toHaveBeenCalledTimes(1);
78+
expect(retry.mock.calls[0][0]).toBe(flows[1]);
79+
80+
const finalState = loadState(statePath);
81+
expect(Object.values(finalState.flows)).toEqual([
82+
{status: 'passed', attempts: 1},
83+
{status: 'passed', attempts: 2},
84+
{status: 'passed', attempts: 1},
85+
]);
86+
});
87+
});

0 commit comments

Comments
 (0)