Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 19 additions & 2 deletions .github/workflows/ios-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ jobs:
if: inputs.E2E_MODE == 'test'
run: |
npx detox --version
yarn detox test --configuration ios.debug.ci --take-screenshots failing --record-logs failing e2e/firstTest.e2e.js
yarn detox test --configuration ios.debug.ci --take-screenshots failing --record-logs failing --capture-view-hierarchy enabled e2e/firstTest.e2e.js

# Informational only, the snapshots are compared by the percy job
- name: Capture visual snapshots
Expand Down Expand Up @@ -196,12 +196,29 @@ jobs:
path: ${{ runner.temp }}/percy-doc-screenshots
if-no-files-found: ignore

# e2e/environment.js has already captured the runner state at the moment a test failed
- name: Collect simulator diagnostics on failure
if: failure()
continue-on-error: true
run: |
mkdir -p artifacts/diagnostics/crash-reports
cp ~/Library/Logs/DiagnosticReports/RNMapboxExample* artifacts/diagnostics/crash-reports/ 2>/dev/null || true
uptime > artifacts/diagnostics/uptime.txt
/usr/bin/log show --last 15m --style compact --predicate 'process == "CoreSimulatorService" OR process == "simctl"' \
> artifacts/diagnostics/host-coresimulator.log 2>&1 || true
if [ -n "$DETOX_DEVICE_UDID" ]; then
xcrun simctl spawn "$DETOX_DEVICE_UDID" log show --last 15m --style compact \
--predicate 'process == "RNMapboxExample" OR process == "SpringBoard"' \
> artifacts/diagnostics/simulator.log 2>&1 || true
fi

- name: Store Detox artifacts on test failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: detox-artifacts
name: detox-artifacts-attempt-${{ github.run_attempt }}
path: example/artifacts
retention-days: 14

percy:
name: Percy visual snapshots
Expand Down
2 changes: 1 addition & 1 deletion example/e2e/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ module.exports = {
reporters: ['detox/runners/jest/reporter'],
globalSetup: 'detox/runners/jest/globalSetup',
globalTeardown: 'detox/runners/jest/globalTeardown',
testEnvironment: 'detox/runners/jest/testEnvironment',
testEnvironment: '<rootDir>/e2e/environment.js',
};
89 changes: 89 additions & 0 deletions example/e2e/environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* @file Detox test environment that, on CI, snapshots the runner when a test or hook
* fails, while the app is still in the failing state, into
* artifacts/diagnostics/ (uploaded with the Detox artifacts).
*/

const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');

const DetoxCircusEnvironment = require('detox/runners/jest/testEnvironment');

const run = promisify(execFile);

const OUT_DIR = path.join(__dirname, '..', 'artifacts', 'diagnostics');
const MAX_CAPTURES = 3;

async function capture(file, cmd, args, timeout = 20000) {
let text;
try {
const { stdout, stderr } = await run(cmd, args, {
timeout,
maxBuffer: 64 * 1024 * 1024,
});
text = stdout + stderr;
} catch (e) {
text = `${e.stdout || ''}${e.stderr || ''}\n${e.message}`;
}
fs.writeFileSync(file, `$ ${cmd} ${args.join(' ')}\n${text}`);
}

class DiagnosticsEnvironment extends DetoxCircusEnvironment {
constructor(config, context) {
super(config, context);
this.captures = 0;
}

async handleTestEvent(event, state) {
await super.handleTestEvent(event, state);
if (
process.env.CI &&
(event.name === 'test_fn_failure' || event.name === 'hook_failure') &&
this.captures < MAX_CAPTURES
) {
this.captures += 1;
const name = event.test?.name ?? event.hook?.type ?? 'unknown';
await this.captureDiagnostics(
`${path.basename(this.testPath, '.e2e.js')}-${this.captures}-${name}`,
);
}
}

async captureDiagnostics(label) {
const dir = path.join(OUT_DIR, label.replace(/[^\w.-]+/g, '_'));
fs.mkdirSync(dir, { recursive: true });
const udid = process.env.DETOX_DEVICE_UDID;

await Promise.all([
// Host processes by CPU, including any simctl/applesimutils call Detox is stuck on
capture(path.join(dir, 'ps.txt'), 'ps', [
'-Ao',
'pid,ppid,etime,pcpu,pmem,command',
'-r',
]),
capture(path.join(dir, 'vm_stat.txt'), 'vm_stat', []),
// Connect/TLS timings from the runner, for map loads stuck on a style request
capture(path.join(dir, 'curl-api.mapbox.com.txt'), 'curl', [
'-sS',
'-o',
'/dev/null',
'--max-time',
'15',
'-w',
'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} http=%{http_code} ip=%{remote_ip}\n',
'https://api.mapbox.com/',
]),
udid &&
capture(path.join(dir, 'simctl-list.txt'), 'xcrun', [
'simctl',
'list',
'devices',
'booted',
]),
]);
}
}

module.exports = DiagnosticsEnvironment;
Loading