@tamagui/native-ci 2.1.0-1780964419737 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detox.mjs +113 -11
- package/dist/detox.mjs.map +1 -1
- package/package.json +2 -2
- package/src/detox.ts +180 -18
- package/types/detox.d.ts +24 -1
- package/types/detox.d.ts.map +1 -1
package/dist/detox.mjs
CHANGED
|
@@ -70,6 +70,43 @@ function buildDetoxArgs(options) {
|
|
|
70
70
|
return args;
|
|
71
71
|
}
|
|
72
72
|
const DETOX_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
73
|
+
const MAX_CONNECT_FLAKE_RETRIES = 1;
|
|
74
|
+
const CONNECT_FLAKE_SIGNATURES = [/Exceeded timeout of \d+ ms for a hook/, /\btimed out after \d+ms/, /could not connect to Detox/, /launch recovery deadline exceeded/, /skipping launchApp:/, /skipping reload:/];
|
|
75
|
+
const ANSI_PATTERN = /\[[0-9;]*m/g;
|
|
76
|
+
function classifyDetoxFailures(rawOutput) {
|
|
77
|
+
const output = rawOutput.replace(ANSI_PATTERN, "");
|
|
78
|
+
const delimiter = /(PASS|FAIL) (e2e\/[^\s):]+\.test\.ts)/g;
|
|
79
|
+
const marks = [];
|
|
80
|
+
for (const m of output.matchAll(delimiter)) {
|
|
81
|
+
marks.push({
|
|
82
|
+
kind: m[1],
|
|
83
|
+
file: m[2],
|
|
84
|
+
index: m.index ?? 0
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const failedFiles = [];
|
|
88
|
+
const flakeFiles = [];
|
|
89
|
+
const realFiles = [];
|
|
90
|
+
for (let i = 0; i < marks.length; i++) {
|
|
91
|
+
const mark = marks[i];
|
|
92
|
+
if (mark.kind !== "FAIL") continue;
|
|
93
|
+
const blockEnd = i + 1 < marks.length ? marks[i + 1].index : output.length;
|
|
94
|
+
const block = output.slice(mark.index, blockEnd);
|
|
95
|
+
if (failedFiles.includes(mark.file)) continue;
|
|
96
|
+
failedFiles.push(mark.file);
|
|
97
|
+
const isFlake = CONNECT_FLAKE_SIGNATURES.some(sig => sig.test(block));
|
|
98
|
+
if (isFlake) {
|
|
99
|
+
flakeFiles.push(mark.file);
|
|
100
|
+
} else {
|
|
101
|
+
realFiles.push(mark.file);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
failedFiles,
|
|
106
|
+
flakeFiles,
|
|
107
|
+
realFiles
|
|
108
|
+
};
|
|
109
|
+
}
|
|
73
110
|
async function resetDetoxLockFile() {
|
|
74
111
|
console.info("Resetting Detox lock file...");
|
|
75
112
|
const proc = Bun.spawn(["npx", "detox", "reset-lock-file"], {
|
|
@@ -78,9 +115,7 @@ async function resetDetoxLockFile() {
|
|
|
78
115
|
});
|
|
79
116
|
await proc.exited;
|
|
80
117
|
}
|
|
81
|
-
async function
|
|
82
|
-
await resetDetoxLockFile();
|
|
83
|
-
const detoxArgs = buildDetoxArgs(options);
|
|
118
|
+
async function spawnDetoxOnce(detoxArgs) {
|
|
84
119
|
console.info("\n--- Running Detox tests ---");
|
|
85
120
|
console.info(`Using fixed Detox server port: ${DETOX_SERVER_PORT}`);
|
|
86
121
|
console.info(`Command: npx ${detoxArgs.join(" ")}`);
|
|
@@ -89,9 +124,26 @@ async function runDetoxTests(options) {
|
|
|
89
124
|
...process.env,
|
|
90
125
|
DETOX_SERVER_PORT: String(DETOX_SERVER_PORT)
|
|
91
126
|
},
|
|
92
|
-
stdout: "
|
|
93
|
-
stderr: "
|
|
127
|
+
stdout: "pipe",
|
|
128
|
+
stderr: "pipe"
|
|
94
129
|
});
|
|
130
|
+
const decoder = new TextDecoder();
|
|
131
|
+
let output = "";
|
|
132
|
+
const tee = async (stream, sink) => {
|
|
133
|
+
const reader = stream.getReader();
|
|
134
|
+
while (true) {
|
|
135
|
+
const {
|
|
136
|
+
done,
|
|
137
|
+
value
|
|
138
|
+
} = await reader.read();
|
|
139
|
+
if (done) break;
|
|
140
|
+
sink.write(value);
|
|
141
|
+
output += decoder.decode(value, {
|
|
142
|
+
stream: true
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const pumps = Promise.all([tee(proc.stdout, process.stdout), tee(proc.stderr, process.stderr)]);
|
|
95
147
|
const timeoutPromise = new Promise(resolve => {
|
|
96
148
|
setTimeout(() => resolve("timeout"), DETOX_TIMEOUT_MS);
|
|
97
149
|
});
|
|
@@ -99,16 +151,66 @@ async function runDetoxTests(options) {
|
|
|
99
151
|
if (result === "timeout") {
|
|
100
152
|
console.info("\nDetox timed out, killing process...");
|
|
101
153
|
proc.kill("SIGKILL");
|
|
102
|
-
return
|
|
154
|
+
return {
|
|
155
|
+
exitCode: 1,
|
|
156
|
+
output
|
|
157
|
+
};
|
|
103
158
|
}
|
|
104
|
-
|
|
159
|
+
await pumps;
|
|
160
|
+
return {
|
|
161
|
+
exitCode: result,
|
|
162
|
+
output
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function runDetoxTests(options) {
|
|
166
|
+
await resetDetoxLockFile();
|
|
167
|
+
const detoxArgs = buildDetoxArgs(options);
|
|
168
|
+
const {
|
|
169
|
+
exitCode,
|
|
170
|
+
output
|
|
171
|
+
} = await spawnDetoxOnce(detoxArgs);
|
|
105
172
|
if (exitCode === 0) {
|
|
106
173
|
console.info("\nAll tests passed!");
|
|
107
|
-
|
|
108
|
-
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
console.info(`
|
|
109
177
|
Tests failed with exit code: ${exitCode}`);
|
|
178
|
+
const {
|
|
179
|
+
flakeFiles,
|
|
180
|
+
realFiles
|
|
181
|
+
} = classifyDetoxFailures(output);
|
|
182
|
+
if (flakeFiles.length === 0) {
|
|
183
|
+
return exitCode;
|
|
184
|
+
}
|
|
185
|
+
if (realFiles.length > 0) {
|
|
186
|
+
console.info(`
|
|
187
|
+
Real test failure(s) present (${realFiles.join(", ")}); not retrying the connect-flaked file(s).`);
|
|
188
|
+
return exitCode;
|
|
189
|
+
}
|
|
190
|
+
for (let attempt = 1; attempt <= MAX_CONNECT_FLAKE_RETRIES; attempt++) {
|
|
191
|
+
console.info(`
|
|
192
|
+
::warning::Detox launch-connect flake detected (beforeAll could not connect to Detox). Retry ${attempt}/${MAX_CONNECT_FLAKE_RETRIES} of: ${flakeFiles.join(", ")}`);
|
|
193
|
+
await resetDetoxLockFile();
|
|
194
|
+
const retryArgs = buildDetoxArgs({
|
|
195
|
+
...options,
|
|
196
|
+
testFiles: flakeFiles
|
|
197
|
+
});
|
|
198
|
+
const retry = await spawnDetoxOnce(retryArgs);
|
|
199
|
+
if (retry.exitCode === 0) {
|
|
200
|
+
console.info(`
|
|
201
|
+
\u2713 Recovered ${flakeFiles.length} connect-flaked file(s) on retry ${attempt}.`);
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
const retryClass = classifyDetoxFailures(retry.output);
|
|
205
|
+
if (retryClass.realFiles.length > 0 || retryClass.flakeFiles.length === 0) {
|
|
206
|
+
console.info("\nRetry surfaced a non-flake failure; surfacing it.");
|
|
207
|
+
return retry.exitCode;
|
|
208
|
+
}
|
|
209
|
+
flakeFiles.length = 0;
|
|
210
|
+
flakeFiles.push(...retryClass.flakeFiles);
|
|
110
211
|
}
|
|
111
|
-
|
|
212
|
+
console.info("\nConnect-flake retries exhausted; tests still failing.");
|
|
213
|
+
return 1;
|
|
112
214
|
}
|
|
113
|
-
export { buildDetoxArgs, parseDetoxArgs, resetDetoxLockFile, runDetoxTests };
|
|
215
|
+
export { buildDetoxArgs, classifyDetoxFailures, parseDetoxArgs, resetDetoxLockFile, runDetoxTests };
|
|
114
216
|
//# sourceMappingURL=detox.mjs.map
|
package/dist/detox.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["parseArgs","DETOX_SERVER_PORT","parseDetoxArgs","platform","defaultConfig","values","positionals","options","config","type","default","process","cwd","headless","retries","workers","allowPositionals","retriesNum","Number","parseInt","isNaN","console","error","exit","workersNum","projectRoot","recordLogs","testFiles","length","buildDetoxArgs","args","String","push","DETOX_TIMEOUT_MS","resetDetoxLockFile","info","proc","Bun","spawn","stdout","stderr","exited","
|
|
1
|
+
{"version":3,"names":["parseArgs","DETOX_SERVER_PORT","parseDetoxArgs","platform","defaultConfig","values","positionals","options","config","type","default","process","cwd","headless","retries","workers","allowPositionals","retriesNum","Number","parseInt","isNaN","console","error","exit","workersNum","projectRoot","recordLogs","testFiles","length","buildDetoxArgs","args","String","push","DETOX_TIMEOUT_MS","MAX_CONNECT_FLAKE_RETRIES","CONNECT_FLAKE_SIGNATURES","ANSI_PATTERN","classifyDetoxFailures","rawOutput","output","replace","delimiter","marks","m","matchAll","kind","file","index","failedFiles","flakeFiles","realFiles","i","mark","blockEnd","block","slice","includes","isFlake","some","sig","test","resetDetoxLockFile","info","proc","Bun","spawn","stdout","stderr","exited","spawnDetoxOnce","detoxArgs","join","env","decoder","TextDecoder","tee","stream","sink","reader","getReader","done","value","read","write","decode","pumps","Promise","all","timeoutPromise","resolve","setTimeout","result","race","kill","exitCode","runDetoxTests","attempt","retryArgs","retry","retryClass"],"sources":["../src/detox.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/native-ci",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Native CI/CD helpers for React Native apps with Expo - fingerprinting, caching, and build optimization",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@expo/fingerprint": "~0.16.6"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@tamagui/build": "2.
|
|
45
|
+
"@tamagui/build": "2.2.0",
|
|
46
46
|
"@types/bun": "^1.1.0",
|
|
47
47
|
"@types/node": "^22.1.0"
|
|
48
48
|
},
|
package/src/detox.ts
CHANGED
|
@@ -107,6 +107,84 @@ export function buildDetoxArgs(options: DetoxRunnerOptions): string[] {
|
|
|
107
107
|
// workflow job timeout is 90min, leaving room for setup + this run.
|
|
108
108
|
const DETOX_TIMEOUT_MS = 60 * 60 * 1000
|
|
109
109
|
|
|
110
|
+
// Connect-flake retry: a single beforeAll launchApp that can't connect to Detox on a
|
|
111
|
+
// degraded runner fails the whole spec file (all its tests fail off the one hook), and
|
|
112
|
+
// nothing retries beforeAll (jest.retryTimes only re-runs individual tests; detox
|
|
113
|
+
// --retries is held at 0 to avoid blanket whole-file retry wall-time). So we retry the
|
|
114
|
+
// failed files ONCE, but ONLY when every failed file's failure carries the launch/connect
|
|
115
|
+
// flake signature - never on a real assertion failure. This is safe by construction: a
|
|
116
|
+
// real failure fails again on retry, so a misclassification can only cost one extra file
|
|
117
|
+
// run, never mask a genuine bug. See memory project_detox_app_connect_runaway.
|
|
118
|
+
const MAX_CONNECT_FLAKE_RETRIES = 1
|
|
119
|
+
|
|
120
|
+
// Signatures that mark a spec file's failure as the launch/connect flake rather than a
|
|
121
|
+
// real test failure. Matched against the ANSI-stripped per-file FAIL block.
|
|
122
|
+
// - "for a hook" - jest hook (beforeAll/beforeEach) timeout; real test timeouts say
|
|
123
|
+
// "for a test", so this never matches an assertion failure.
|
|
124
|
+
// - "timed out after <n>ms" - lowercase message thrown by our withTimeout() wrapper,
|
|
125
|
+
// used ONLY by safeLaunchApp/safeReloadApp/safeTerminate; detox's own waitFor emits
|
|
126
|
+
// capital "Timed out after <n>ms waiting for", so the lowercase form is launch-only.
|
|
127
|
+
// - the safeLaunchApp/safeReloadApp breaker messages.
|
|
128
|
+
const CONNECT_FLAKE_SIGNATURES: RegExp[] = [
|
|
129
|
+
/Exceeded timeout of \d+ ms for a hook/,
|
|
130
|
+
/\btimed out after \d+ms/,
|
|
131
|
+
/could not connect to Detox/,
|
|
132
|
+
/launch recovery deadline exceeded/,
|
|
133
|
+
/skipping launchApp:/,
|
|
134
|
+
/skipping reload:/,
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const ANSI_PATTERN = /\[[0-9;]*m/g
|
|
138
|
+
|
|
139
|
+
interface DetoxFailureClassification {
|
|
140
|
+
/** every spec file with a FAIL line */
|
|
141
|
+
failedFiles: string[]
|
|
142
|
+
/** failed files whose failure matches a connect-flake signature (retry candidates) */
|
|
143
|
+
flakeFiles: string[]
|
|
144
|
+
/** failed files whose failure looks real (assertion/logic) - never retried */
|
|
145
|
+
realFiles: string[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Classify a detox/jest run's combined output into connect-flake vs real failures,
|
|
150
|
+
* per spec file. jest prints each file's `FAIL e2e/X.test.ts` line followed by that
|
|
151
|
+
* file's failure detail (the ● blocks) before the next PASS/FAIL line, so the text
|
|
152
|
+
* between one FAIL line and the next delimiter is that file's failure block.
|
|
153
|
+
*
|
|
154
|
+
* Exported for unit testing against real CI logs.
|
|
155
|
+
*/
|
|
156
|
+
export function classifyDetoxFailures(rawOutput: string): DetoxFailureClassification {
|
|
157
|
+
const output = rawOutput.replace(ANSI_PATTERN, '')
|
|
158
|
+
// delimiter = any "PASS e2e/..." or "FAIL e2e/..." occurrence (not anchored: real CI
|
|
159
|
+
// logs may carry a leading timestamp/prefix on the line).
|
|
160
|
+
const delimiter = /(PASS|FAIL) (e2e\/[^\s):]+\.test\.ts)/g
|
|
161
|
+
const marks: { kind: string; file: string; index: number }[] = []
|
|
162
|
+
for (const m of output.matchAll(delimiter)) {
|
|
163
|
+
marks.push({ kind: m[1], file: m[2], index: m.index ?? 0 })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const failedFiles: string[] = []
|
|
167
|
+
const flakeFiles: string[] = []
|
|
168
|
+
const realFiles: string[] = []
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < marks.length; i++) {
|
|
171
|
+
const mark = marks[i]
|
|
172
|
+
if (mark.kind !== 'FAIL') continue
|
|
173
|
+
const blockEnd = i + 1 < marks.length ? marks[i + 1].index : output.length
|
|
174
|
+
const block = output.slice(mark.index, blockEnd)
|
|
175
|
+
if (failedFiles.includes(mark.file)) continue
|
|
176
|
+
failedFiles.push(mark.file)
|
|
177
|
+
const isFlake = CONNECT_FLAKE_SIGNATURES.some((sig) => sig.test(block))
|
|
178
|
+
if (isFlake) {
|
|
179
|
+
flakeFiles.push(mark.file)
|
|
180
|
+
} else {
|
|
181
|
+
realFiles.push(mark.file)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { failedFiles, flakeFiles, realFiles }
|
|
186
|
+
}
|
|
187
|
+
|
|
110
188
|
/**
|
|
111
189
|
* Reset Detox lock file to prevent ECOMPROMISED errors in CI
|
|
112
190
|
* See: https://github.com/wix/Detox/issues/4210
|
|
@@ -121,29 +199,46 @@ export async function resetDetoxLockFile(): Promise<void> {
|
|
|
121
199
|
}
|
|
122
200
|
|
|
123
201
|
/**
|
|
124
|
-
*
|
|
202
|
+
* Spawn one `npx detox test ...` invocation, streaming its output live to the CI log
|
|
203
|
+
* while also capturing it (ANSI included) so we can classify failures afterwards.
|
|
125
204
|
*
|
|
126
|
-
* @returns
|
|
205
|
+
* @returns the process exit code and the full combined stdout+stderr text.
|
|
127
206
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
await resetDetoxLockFile()
|
|
132
|
-
|
|
133
|
-
const detoxArgs = buildDetoxArgs(options)
|
|
134
|
-
|
|
207
|
+
async function spawnDetoxOnce(
|
|
208
|
+
detoxArgs: string[]
|
|
209
|
+
): Promise<{ exitCode: number; output: string }> {
|
|
135
210
|
console.info('\n--- Running Detox tests ---')
|
|
136
211
|
console.info(`Using fixed Detox server port: ${DETOX_SERVER_PORT}`)
|
|
137
212
|
console.info(`Command: npx ${detoxArgs.join(' ')}`)
|
|
138
213
|
|
|
139
|
-
//
|
|
214
|
+
// pipe (not inherit) so we can capture output for flake classification; tee each
|
|
215
|
+
// chunk straight back to our stdout/stderr so the live CI log is unchanged.
|
|
140
216
|
const proc = Bun.spawn(['npx', ...detoxArgs], {
|
|
141
217
|
env: { ...process.env, DETOX_SERVER_PORT: String(DETOX_SERVER_PORT) },
|
|
142
|
-
stdout: '
|
|
143
|
-
stderr: '
|
|
218
|
+
stdout: 'pipe',
|
|
219
|
+
stderr: 'pipe',
|
|
144
220
|
})
|
|
145
221
|
|
|
146
|
-
|
|
222
|
+
const decoder = new TextDecoder()
|
|
223
|
+
let output = ''
|
|
224
|
+
const tee = async (
|
|
225
|
+
stream: ReadableStream<Uint8Array>,
|
|
226
|
+
sink: NodeJS.WriteStream
|
|
227
|
+
): Promise<void> => {
|
|
228
|
+
const reader = stream.getReader()
|
|
229
|
+
while (true) {
|
|
230
|
+
const { done, value } = await reader.read()
|
|
231
|
+
if (done) break
|
|
232
|
+
sink.write(value)
|
|
233
|
+
output += decoder.decode(value, { stream: true })
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const pumps = Promise.all([
|
|
238
|
+
tee(proc.stdout, process.stdout),
|
|
239
|
+
tee(proc.stderr, process.stderr),
|
|
240
|
+
])
|
|
241
|
+
|
|
147
242
|
const timeoutPromise = new Promise<'timeout'>((resolve) => {
|
|
148
243
|
setTimeout(() => resolve('timeout'), DETOX_TIMEOUT_MS)
|
|
149
244
|
})
|
|
@@ -153,16 +248,83 @@ export async function runDetoxTests(options: DetoxRunnerOptions): Promise<number
|
|
|
153
248
|
if (result === 'timeout') {
|
|
154
249
|
console.info('\nDetox timed out, killing process...')
|
|
155
250
|
proc.kill('SIGKILL')
|
|
156
|
-
return 1
|
|
251
|
+
return { exitCode: 1, output }
|
|
157
252
|
}
|
|
158
253
|
|
|
159
|
-
|
|
254
|
+
// drain any buffered output the process emitted right before exit
|
|
255
|
+
await pumps
|
|
256
|
+
return { exitCode: result, output }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run Detox tests with the given options.
|
|
261
|
+
*
|
|
262
|
+
* On failure, retries the failed spec files ONCE when (and only when) every failed
|
|
263
|
+
* file's failure carries the launch/connect flake signature - this recovers the
|
|
264
|
+
* beforeAll-launch flake that jest.retryTimes can't, without reintroducing blanket
|
|
265
|
+
* whole-file retries. Real failures are never retried (and would fail again anyway).
|
|
266
|
+
*
|
|
267
|
+
* @returns Exit code from Detox
|
|
268
|
+
*/
|
|
269
|
+
export async function runDetoxTests(options: DetoxRunnerOptions): Promise<number> {
|
|
270
|
+
// Reset lock file to prevent ECOMPROMISED errors in CI
|
|
271
|
+
// This clears stale locks from previous runs that can cause "Unable to update lock within the stale threshold" errors
|
|
272
|
+
await resetDetoxLockFile()
|
|
273
|
+
|
|
274
|
+
const detoxArgs = buildDetoxArgs(options)
|
|
275
|
+
const { exitCode, output } = await spawnDetoxOnce(detoxArgs)
|
|
160
276
|
|
|
161
277
|
if (exitCode === 0) {
|
|
162
278
|
console.info('\nAll tests passed!')
|
|
163
|
-
|
|
164
|
-
|
|
279
|
+
return 0
|
|
280
|
+
}
|
|
281
|
+
console.info(`\nTests failed with exit code: ${exitCode}`)
|
|
282
|
+
|
|
283
|
+
const { flakeFiles, realFiles } = classifyDetoxFailures(output)
|
|
284
|
+
|
|
285
|
+
// nothing to retry: either we couldn't attribute the failure to specific files, or a
|
|
286
|
+
// real (non-flake) failure is present - in both cases surface the original result.
|
|
287
|
+
if (flakeFiles.length === 0) {
|
|
288
|
+
return exitCode
|
|
289
|
+
}
|
|
290
|
+
if (realFiles.length > 0) {
|
|
291
|
+
console.info(
|
|
292
|
+
`\nReal test failure(s) present (${realFiles.join(', ')}); not retrying the ` +
|
|
293
|
+
`connect-flaked file(s).`
|
|
294
|
+
)
|
|
295
|
+
return exitCode
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (let attempt = 1; attempt <= MAX_CONNECT_FLAKE_RETRIES; attempt++) {
|
|
299
|
+
console.info(
|
|
300
|
+
`\n::warning::Detox launch-connect flake detected (beforeAll could not connect to ` +
|
|
301
|
+
`Detox). Retry ${attempt}/${MAX_CONNECT_FLAKE_RETRIES} of: ${flakeFiles.join(', ')}`
|
|
302
|
+
)
|
|
303
|
+
// fresh lock file before the retry so a stale lock from the wedged run can't
|
|
304
|
+
// immediately re-trip the same connect failure.
|
|
305
|
+
await resetDetoxLockFile()
|
|
306
|
+
|
|
307
|
+
const retryArgs = buildDetoxArgs({ ...options, testFiles: flakeFiles })
|
|
308
|
+
const retry = await spawnDetoxOnce(retryArgs)
|
|
309
|
+
if (retry.exitCode === 0) {
|
|
310
|
+
console.info(
|
|
311
|
+
`\n✓ Recovered ${flakeFiles.length} connect-flaked file(s) on retry ${attempt}.`
|
|
312
|
+
)
|
|
313
|
+
return 0
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// if the retry surfaced a real failure (the flake cleared but a genuine bug is
|
|
317
|
+
// underneath), stop retrying and surface it.
|
|
318
|
+
const retryClass = classifyDetoxFailures(retry.output)
|
|
319
|
+
if (retryClass.realFiles.length > 0 || retryClass.flakeFiles.length === 0) {
|
|
320
|
+
console.info('\nRetry surfaced a non-flake failure; surfacing it.')
|
|
321
|
+
return retry.exitCode
|
|
322
|
+
}
|
|
323
|
+
// still a pure connect-flake - loop will retry again if budget remains.
|
|
324
|
+
flakeFiles.length = 0
|
|
325
|
+
flakeFiles.push(...retryClass.flakeFiles)
|
|
165
326
|
}
|
|
166
327
|
|
|
167
|
-
|
|
328
|
+
console.info('\nConnect-flake retries exhausted; tests still failing.')
|
|
329
|
+
return 1
|
|
168
330
|
}
|
package/types/detox.d.ts
CHANGED
|
@@ -36,15 +36,38 @@ export declare function parseDetoxArgs(platform: Platform): {
|
|
|
36
36
|
* Build Detox CLI command arguments
|
|
37
37
|
*/
|
|
38
38
|
export declare function buildDetoxArgs(options: DetoxRunnerOptions): string[];
|
|
39
|
+
interface DetoxFailureClassification {
|
|
40
|
+
/** every spec file with a FAIL line */
|
|
41
|
+
failedFiles: string[];
|
|
42
|
+
/** failed files whose failure matches a connect-flake signature (retry candidates) */
|
|
43
|
+
flakeFiles: string[];
|
|
44
|
+
/** failed files whose failure looks real (assertion/logic) - never retried */
|
|
45
|
+
realFiles: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classify a detox/jest run's combined output into connect-flake vs real failures,
|
|
49
|
+
* per spec file. jest prints each file's `FAIL e2e/X.test.ts` line followed by that
|
|
50
|
+
* file's failure detail (the ● blocks) before the next PASS/FAIL line, so the text
|
|
51
|
+
* between one FAIL line and the next delimiter is that file's failure block.
|
|
52
|
+
*
|
|
53
|
+
* Exported for unit testing against real CI logs.
|
|
54
|
+
*/
|
|
55
|
+
export declare function classifyDetoxFailures(rawOutput: string): DetoxFailureClassification;
|
|
39
56
|
/**
|
|
40
57
|
* Reset Detox lock file to prevent ECOMPROMISED errors in CI
|
|
41
58
|
* See: https://github.com/wix/Detox/issues/4210
|
|
42
59
|
*/
|
|
43
60
|
export declare function resetDetoxLockFile(): Promise<void>;
|
|
44
61
|
/**
|
|
45
|
-
* Run Detox tests with the given options
|
|
62
|
+
* Run Detox tests with the given options.
|
|
63
|
+
*
|
|
64
|
+
* On failure, retries the failed spec files ONCE when (and only when) every failed
|
|
65
|
+
* file's failure carries the launch/connect flake signature - this recovers the
|
|
66
|
+
* beforeAll-launch flake that jest.retryTimes can't, without reintroducing blanket
|
|
67
|
+
* whole-file retries. Real failures are never retried (and would fail again anyway).
|
|
46
68
|
*
|
|
47
69
|
* @returns Exit code from Detox
|
|
48
70
|
*/
|
|
49
71
|
export declare function runDetoxTests(options: DetoxRunnerOptions): Promise<number>;
|
|
72
|
+
export {};
|
|
50
73
|
//# sourceMappingURL=detox.d.ts.map
|
package/types/detox.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detox.d.ts","sourceRoot":"","sources":["../src/detox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ;;;;;;;;EAsChD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,EAAE,CA6BpE;
|
|
1
|
+
{"version":3,"file":"detox.d.ts","sourceRoot":"","sources":["../src/detox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ;;;;;;;;EAsChD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,EAAE,CA6BpE;AAoCD,UAAU,0BAA0B;IAClC,uCAAuC;IACvC,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,sFAAsF;IACtF,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,0BAA0B,CA8BnF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOxD;AA4DD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6DhF"}
|