Skip to content

Commit 4da17eb

Browse files
committed
feat: physical device
1 parent 5678119 commit 4da17eb

12 files changed

Lines changed: 233 additions & 57 deletions

File tree

packages/jest/src/__tests__/errors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('NativeCrashError', () => {
2626
it('omits single-line iOS summaries from the rendered error message', () => {
2727
const error = new NativeCrashError('/tmp/crash.harness.ts', {
2828
phase: 'startup',
29-
artifactType: 'ios-simulator-crash-report',
29+
artifactType: 'ios-crash-report',
3030
summary:
3131
'2026-03-12 13:46:18.154 Df HarnessPlayground[18007:65e716] [com.apple.dt.xctest:Default] notify_get_state check indicated test daemon not ready.',
3232
processName: 'HarnessPlayground',

packages/jest/src/errors.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ const buildNativeCrashMessage = ({
4848
Boolean(summary) &&
4949
!(
5050
!hasCrashBlock &&
51-
(artifactType === 'ios-libimobiledevice-crash-report' ||
52-
artifactType === 'ios-simulator-crash-report')
51+
artifactType === 'ios-crash-report'
5352
);
5453

5554
if (shouldRenderSummary && summary) {

packages/platform-ios/src/__tests__/app-monitor.test.ts

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,25 @@ describe('createUnifiedLogEvent', () => {
5656
});
5757
});
5858

59-
it('detects Swift fatal errors as crash signals', () => {
59+
it('detects Swift fatal errors from idevicesyslog with library-qualified process name', () => {
60+
const event = createUnifiedLogEvent({
61+
line: 'Mar 13 12:27:13.724837 HarnessPlayground(libswiftCore.dylib)[21675] <Notice>: HarnessPlayground/AppDelegate.swift:31: Fatal error: Intentional pre-RN startup crash',
62+
processNames: ['HarnessPlayground', 'com.harnessplayground'],
63+
});
64+
65+
expect(event).toMatchObject({
66+
type: 'possible_crash',
67+
source: 'logs',
68+
isConfirmed: true,
69+
crashDetails: {
70+
source: 'logs',
71+
processName: 'HarnessPlayground',
72+
pid: 21675,
73+
},
74+
});
75+
});
76+
77+
it('detects Swift fatal errors from simulator logs', () => {
6078
const event = createUnifiedLogEvent({
6179
line: '2026-03-13 10:29:13.868 Df HarnessPlayground[34784:8f92b3] (libswiftCore.dylib) HarnessPlayground/AppDelegate.swift:31: Fatal error: Intentional pre-RN startup crash',
6280
processNames: ['HarnessPlayground', 'com.harnessplayground'],
@@ -201,10 +219,10 @@ describe('createIosSimulatorAppMonitor', () => {
201219
vi.spyOn(simctl, 'collectCrashReports').mockImplementation(
202220
async ({ crashArtifactWriter }) => [
203221
{
204-
artifactType: 'ios-simulator-crash-report',
222+
artifactType: 'ios-crash-report',
205223
artifactPath:
206224
crashArtifactWriter?.persistArtifact({
207-
artifactKind: 'ios-simulator-crash-report',
225+
artifactKind: 'ios-crash-report',
208226
source: {
209227
kind: 'file',
210228
path: sourcePath,
@@ -251,7 +269,7 @@ describe('createIosSimulatorAppMonitor', () => {
251269
await monitor.stop();
252270

253271
expect(details).toMatchObject({
254-
artifactType: 'ios-simulator-crash-report',
272+
artifactType: 'ios-crash-report',
255273
summary: 'simulator crash report',
256274
});
257275
expect(details?.artifactPath).toContain('/.harness/crash-reports/');
@@ -285,7 +303,7 @@ describe('createIosSimulatorAppMonitor', () => {
285303

286304
return [
287305
{
288-
artifactType: 'ios-simulator-crash-report',
306+
artifactType: 'ios-crash-report',
289307
artifactPath: '/tmp/HarnessPlayground.ips',
290308
occurredAt: Date.now(),
291309
processName: 'HarnessPlayground',
@@ -315,7 +333,7 @@ describe('createIosSimulatorAppMonitor', () => {
315333

316334
expect(calls).toBe(2);
317335
expect(details).toMatchObject({
318-
artifactType: 'ios-simulator-crash-report',
336+
artifactType: 'ios-crash-report',
319337
stackTrace: ['0 AppDelegate.crashIfRequested() (AppDelegate.swift:31)'],
320338
});
321339
});
@@ -402,19 +420,67 @@ describe('createIosDeviceAppMonitor', () => {
402420

403421
const monitor = createIosDeviceAppMonitor({
404422
deviceId: 'device-udid',
423+
libimobiledeviceUdid: 'hardware-udid',
405424
bundleId: 'com.harnessplayground',
406425
});
407426

408427
await monitor.start();
409428
await monitor.stop();
410429

411-
expect(targetSpy).toHaveBeenCalledWith('device-udid');
430+
expect(targetSpy).toHaveBeenCalledWith('hardware-udid');
412431
expect(syslogSpy).toHaveBeenCalledWith({
413-
targetId: 'device-udid',
432+
targetId: 'hardware-udid',
414433
processNames: ['com.harnessplayground', 'HarnessPlayground'],
415434
});
416435
});
417436

437+
it('detects idevicesyslog crash lines with library-qualified process names', async () => {
438+
vi.spyOn(libimobiledevice, 'assertLibimobiledeviceTargetAvailable').mockResolvedValue(
439+
undefined
440+
);
441+
vi.spyOn(libimobiledevice, 'createSyslogProcess').mockReturnValue(
442+
createStreamingSubprocess([
443+
{
444+
line: 'Mar 13 12:27:13.724837 HarnessPlayground(libswiftCore.dylib)[21675] <Notice>: HarnessPlayground/AppDelegate.swift:31: Fatal error: Intentional pre-RN startup crash',
445+
},
446+
])
447+
);
448+
vi.spyOn(libimobiledevice, 'collectCrashReports').mockResolvedValue([]);
449+
vi.spyOn(devicectl, 'getAppInfo').mockResolvedValue({
450+
bundleIdentifier: 'com.harnessplayground',
451+
name: 'HarnessPlayground',
452+
version: '1.0',
453+
url: '/private/var/HarnessPlayground.app',
454+
});
455+
456+
const events: Array<{ type: string }> = [];
457+
const monitor = createIosDeviceAppMonitor({
458+
deviceId: 'device-udid',
459+
libimobiledeviceUdid: 'hardware-udid',
460+
bundleId: 'com.harnessplayground',
461+
});
462+
monitor.addListener((event) => {
463+
events.push(event);
464+
});
465+
466+
await monitor.start();
467+
await new Promise((resolve) => setTimeout(resolve, 10));
468+
469+
const details = await monitor.getCrashDetails({
470+
pid: 21675,
471+
occurredAt: Date.now(),
472+
});
473+
474+
await monitor.stop();
475+
476+
expect(events.some((event) => event.type === 'possible_crash')).toBe(true);
477+
expect(details).toMatchObject({
478+
source: 'logs',
479+
processName: 'HarnessPlayground',
480+
pid: 21675,
481+
});
482+
});
483+
418484
it('still enriches device crashes with pulled crash reports', async () => {
419485
vi.spyOn(libimobiledevice, 'assertLibimobiledeviceTargetAvailable').mockResolvedValue(
420486
undefined
@@ -431,10 +497,10 @@ describe('createIosDeviceAppMonitor', () => {
431497
vi.spyOn(libimobiledevice, 'collectCrashReports').mockImplementation(
432498
async ({ crashArtifactWriter }) => [
433499
{
434-
artifactType: 'ios-libimobiledevice-crash-report',
500+
artifactType: 'ios-crash-report',
435501
artifactPath:
436502
crashArtifactWriter?.persistArtifact({
437-
artifactKind: 'ios-libimobiledevice-crash-report',
503+
artifactKind: 'ios-crash-report',
438504
source: {
439505
kind: 'file',
440506
path: sourcePath,
@@ -459,6 +525,7 @@ describe('createIosDeviceAppMonitor', () => {
459525

460526
const monitor = createIosDeviceAppMonitor({
461527
deviceId: 'device-udid',
528+
libimobiledeviceUdid: 'hardware-udid',
462529
bundleId: 'com.harnessplayground',
463530
crashArtifactWriter: createCrashArtifactWriter({
464531
runnerName: 'ios-device',
@@ -479,7 +546,7 @@ describe('createIosDeviceAppMonitor', () => {
479546
await monitor.stop();
480547

481548
expect(details).toMatchObject({
482-
artifactType: 'ios-libimobiledevice-crash-report',
549+
artifactType: 'ios-crash-report',
483550
summary: 'full crash report',
484551
});
485552
expect(details?.artifactPath).toContain('/.harness/crash-reports/');

packages/platform-ios/src/__tests__/libimobiledevice.test.ts

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,19 @@ describe('assertLibimobiledeviceInstalled', () => {
4040

4141
describe('collectCrashReports', () => {
4242
const workDir = fs.mkdtempSync(join(tmpdir(), 'rn-harness-ios-crash-tests-'));
43+
const artifactRoot = fs.mkdtempSync(join(tmpdir(), 'rn-harness-ios-crash-artifacts-'));
4344

4445
afterEach(() => {
4546
vi.restoreAllMocks();
4647
fs.rmSync(workDir, { recursive: true, force: true });
4748
fs.mkdirSync(workDir, { recursive: true });
49+
fs.rmSync(artifactRoot, { recursive: true, force: true });
50+
fs.mkdirSync(artifactRoot, { recursive: true });
4851
});
4952

5053
it('extracts matching crash reports with artifact metadata', async () => {
5154
vi.spyOn(fs, 'mkdtempSync').mockReturnValue(workDir);
52-
vi.spyOn(tools, 'spawn').mockImplementation(
55+
const spawnSpy = vi.spyOn(tools, 'spawn').mockImplementation(
5356
(async (file: string, args?: readonly string[]) => {
5457
if (file === 'idevicecrashreport') {
5558
const targetDir = args?.[args.length - 1];
@@ -86,9 +89,18 @@ describe('collectCrashReports', () => {
8689
processNames: ['HarnessPlayground'],
8790
});
8891

92+
expect(spawnSpy).toHaveBeenCalledWith('idevicecrashreport', [
93+
'-u',
94+
'device-udid',
95+
'--keep',
96+
'--extract',
97+
'--filter',
98+
'HarnessPlayground',
99+
expect.any(String),
100+
]);
89101
expect(reports).toHaveLength(1);
90102
expect(reports[0]).toMatchObject({
91-
artifactType: 'ios-libimobiledevice-crash-report',
103+
artifactType: 'ios-crash-report',
92104
processName: 'HarnessPlayground',
93105
pid: 1234,
94106
signal: 'SIGABRT',
@@ -100,6 +112,88 @@ describe('collectCrashReports', () => {
100112
});
101113
});
102114

115+
it('filters by executable name rather than bundle id', async () => {
116+
vi.spyOn(fs, 'mkdtempSync').mockReturnValue(workDir);
117+
const spawnSpy = vi.spyOn(tools, 'spawn').mockResolvedValue({
118+
stdout: '',
119+
} as Awaited<ReturnType<typeof tools.spawn>>);
120+
121+
await collectCrashReports({
122+
targetId: 'device-udid',
123+
bundleId: 'com.harnessplayground',
124+
processNames: ['com.harnessplayground', 'HarnessPlayground'],
125+
});
126+
127+
expect(spawnSpy).toHaveBeenCalledWith('idevicecrashreport', [
128+
'-u',
129+
'device-udid',
130+
'--keep',
131+
'--extract',
132+
'--filter',
133+
'HarnessPlayground',
134+
expect.any(String),
135+
]);
136+
});
137+
138+
it('parses .ips crash reports from the device', async () => {
139+
vi.spyOn(fs, 'mkdtempSync').mockReturnValue(workDir);
140+
vi.spyOn(tools, 'spawn').mockImplementation(
141+
(async (file: string, args?: readonly string[]) => {
142+
if (file === 'idevicecrashreport') {
143+
const targetDir = args?.[args.length - 1];
144+
145+
if (!targetDir) {
146+
throw new Error('missing target dir');
147+
}
148+
149+
const header = JSON.stringify({
150+
app_name: 'HarnessPlayground',
151+
bundleID: 'com.harnessplayground',
152+
});
153+
const body = JSON.stringify({
154+
pid: 21675,
155+
procName: 'HarnessPlayground',
156+
faultingThread: 0,
157+
exception: { type: 'EXC_BREAKPOINT', signal: 'SIGTRAP' },
158+
threads: [
159+
{
160+
frames: [
161+
{ imageIndex: 0, symbol: 'AppDelegate.crashIfRequested()', symbolLocation: 20 },
162+
],
163+
},
164+
],
165+
usedImages: [{ name: 'HarnessPlayground' }],
166+
});
167+
168+
fs.writeFileSync(
169+
join(targetDir, 'HarnessPlayground-2026-03-12-113508.ips'),
170+
`${header}\n${body}`
171+
);
172+
}
173+
174+
return {
175+
stdout: '',
176+
} as Awaited<ReturnType<typeof tools.spawn>>;
177+
}) as typeof tools.spawn
178+
);
179+
180+
const reports = await collectCrashReports({
181+
targetId: 'device-udid',
182+
bundleId: 'com.harnessplayground',
183+
processNames: ['HarnessPlayground'],
184+
});
185+
186+
expect(reports).toHaveLength(1);
187+
expect(reports[0]).toMatchObject({
188+
artifactType: 'ios-crash-report',
189+
processName: 'HarnessPlayground',
190+
pid: 21675,
191+
signal: 'SIGTRAP',
192+
exceptionType: 'EXC_BREAKPOINT',
193+
stackTrace: ['0 AppDelegate.crashIfRequested() (+ 20)'],
194+
});
195+
});
196+
103197
it('persists pulled crash reports before temporary cleanup', async () => {
104198
vi.spyOn(fs, 'mkdtempSync').mockReturnValue(workDir);
105199
vi.spyOn(tools, 'spawn').mockImplementation(
@@ -126,11 +220,11 @@ describe('collectCrashReports', () => {
126220
} as Awaited<ReturnType<typeof tools.spawn>>;
127221
}) as typeof tools.spawn
128222
);
129-
const artifactRoot = join(workDir, '.harness', 'crash-reports');
223+
const crashReportDir = join(artifactRoot, '.harness', 'crash-reports');
130224
const writer = createCrashArtifactWriter({
131225
runnerName: 'ios-device',
132226
platformId: 'ios',
133-
rootDir: artifactRoot,
227+
rootDir: crashReportDir,
134228
runTimestamp: '2026-03-12T11-35-08-000Z',
135229
});
136230

@@ -143,9 +237,7 @@ describe('collectCrashReports', () => {
143237

144238
expect(reports[0]?.artifactPath).toContain('/.harness/crash-reports/');
145239
expect(fs.existsSync(reports[0]!.artifactPath)).toBe(true);
146-
expect(fs.existsSync(join(workDir, 'HarnessPlayground-2026-03-12-113508.crash'))).toBe(
147-
false
148-
);
240+
expect(fs.existsSync(workDir)).toBe(false);
149241
});
150242

151243
it('returns an empty list when no matching crash reports are found', async () => {

packages/platform-ios/src/__tests__/simctl.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('simctl collectCrashReports', () => {
8888

8989
expect(reports).toEqual([
9090
{
91-
artifactType: 'ios-simulator-crash-report',
91+
artifactType: 'ios-crash-report',
9292
artifactPath: join(
9393
diagnosticReportsDir,
9494
'HarnessPlayground-2026-03-12-122756.ips'

0 commit comments

Comments
 (0)