@@ -15,6 +15,7 @@ const fs = require("fs");
1515const path = require ( "path" ) ;
1616const cp = require ( "child_process" ) ;
1717const crypto = require ( "crypto" ) ;
18+ const os = require ( "os" ) ;
1819
1920const projectPath = path . join ( __dirname , "../napi-ios.xcodeproj" ) ;
2021const scheme = "TestRunner" ;
@@ -57,6 +58,7 @@ const launchedMarker = "Application Start!";
5758const junitPrefix = "TKUnit: " ;
5859const junitEndTag = "</testsuites>" ;
5960const consoleLogMarker = "CONSOLE LOG:" ;
61+ const crashReportsDir = path . join ( os . homedir ( ) , "Library" , "Logs" , "DiagnosticReports" ) ;
6062
6163function parseArgs ( ) {
6264 const args = process . argv . slice ( 2 ) . filter ( Boolean ) ;
@@ -175,6 +177,202 @@ function stripConsoleLogPrefix(line) {
175177 return line . slice ( markerIndex + consoleLogMarker . length ) . trimStart ( ) ;
176178}
177179
180+ function quoteForLLDB ( arg ) {
181+ return `"${ String ( arg ) . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' ) } "` ;
182+ }
183+
184+ function getProcessExitStatus ( code , signal ) {
185+ if ( typeof code === "number" ) {
186+ return { code, display : String ( code ) } ;
187+ }
188+
189+ if ( signal ) {
190+ const signalNumber = os . constants . signals [ signal ] ;
191+ if ( typeof signalNumber === "number" ) {
192+ const mappedCode = 128 + signalNumber ;
193+ return { code : mappedCode , display : `${ mappedCode } (signal ${ signal } )` } ;
194+ }
195+
196+ return { code : 1 , display : `signal ${ signal } ` } ;
197+ }
198+
199+ return { code : 1 , display : "unknown" } ;
200+ }
201+
202+ function isLikelyCrash ( code , signal ) {
203+ if ( signal ) {
204+ return true ;
205+ }
206+
207+ return code === 134 || code === 139 ;
208+ }
209+
210+ function readRecentCrashReportForPid ( pid , launchedAtMs ) {
211+ if ( ! pid || ! fs . existsSync ( crashReportsDir ) ) {
212+ return null ;
213+ }
214+
215+ const candidates = fs . readdirSync ( crashReportsDir )
216+ . filter ( ( name ) => name . startsWith ( "TestRunner-" ) && ( name . endsWith ( ".ips" ) || name . endsWith ( ".crash" ) ) )
217+ . map ( ( name ) => {
218+ const fullPath = path . join ( crashReportsDir , name ) ;
219+ let stats ;
220+ try {
221+ stats = fs . statSync ( fullPath ) ;
222+ } catch ( _ ) {
223+ return null ;
224+ }
225+
226+ return {
227+ fullPath,
228+ mtimeMs : stats . mtimeMs
229+ } ;
230+ } )
231+ . filter ( Boolean )
232+ . filter ( ( item ) => item . mtimeMs >= ( launchedAtMs - 5000 ) )
233+ . sort ( ( a , b ) => b . mtimeMs - a . mtimeMs ) ;
234+
235+ const pidMatchers = [
236+ `"pid" : ${ pid } ` ,
237+ `"pid":${ pid } ` ,
238+ `Process: TestRunner [${ pid } ]` ,
239+ `Process: TestRunner [${ pid } ]`
240+ ] ;
241+
242+ for ( const candidate of candidates ) {
243+ let content ;
244+ try {
245+ content = fs . readFileSync ( candidate . fullPath , "utf8" ) ;
246+ } catch ( _ ) {
247+ continue ;
248+ }
249+
250+ if ( pidMatchers . some ( ( matcher ) => content . includes ( matcher ) ) ) {
251+ return { path : candidate . fullPath , content } ;
252+ }
253+ }
254+
255+ return null ;
256+ }
257+
258+ function formatBacktraceFromIPS ( ipsContent ) {
259+ const firstNewline = ipsContent . indexOf ( "\n" ) ;
260+ if ( firstNewline < 0 ) {
261+ return null ;
262+ }
263+
264+ let report ;
265+ try {
266+ report = JSON . parse ( ipsContent . slice ( firstNewline + 1 ) . trim ( ) ) ;
267+ } catch ( _ ) {
268+ return null ;
269+ }
270+
271+ const threads = report . threads || [ ] ;
272+ if ( ! Array . isArray ( threads ) || threads . length === 0 ) {
273+ return null ;
274+ }
275+
276+ const faultingThread = Number . isInteger ( report . faultingThread ) ? report . faultingThread : 0 ;
277+ const images = report . usedImages || [ ] ;
278+ const lines = [ ] ;
279+ const exceptionType = report . exception && report . exception . type ? report . exception . type : "unknown" ;
280+ const exceptionSignal = report . exception && report . exception . signal ? report . exception . signal : "unknown" ;
281+ lines . push ( `Exception: ${ exceptionType } (${ exceptionSignal } )` ) ;
282+ lines . push ( `Faulting thread: ${ faultingThread } ` ) ;
283+
284+ for ( let threadIndex = 0 ; threadIndex < threads . length ; threadIndex ++ ) {
285+ const thread = threads [ threadIndex ] ;
286+ if ( ! thread || ! Array . isArray ( thread . frames ) ) {
287+ continue ;
288+ }
289+
290+ const threadHeader = threadIndex === faultingThread
291+ ? `Thread ${ threadIndex } Crashed${ thread . queue ? ` (${ thread . queue } )` : "" } :`
292+ : `Thread ${ threadIndex } ${ thread . queue ? ` (${ thread . queue } )` : "" } :` ;
293+ lines . push ( threadHeader ) ;
294+
295+ for ( let frameIndex = 0 ; frameIndex < thread . frames . length ; frameIndex ++ ) {
296+ const frame = thread . frames [ frameIndex ] ;
297+ const imageName = ( typeof frame . imageIndex === "number" && images [ frame . imageIndex ] && images [ frame . imageIndex ] . name )
298+ ? images [ frame . imageIndex ] . name
299+ : `image[${ frame . imageIndex ?? "?" } ]` ;
300+ const symbol = frame . symbol || ( typeof frame . imageOffset === "number" ? `0x${ frame . imageOffset . toString ( 16 ) } ` : "<unknown>" ) ;
301+ const symbolLocation = typeof frame . symbolLocation === "number" ? ` + ${ frame . symbolLocation } ` : "" ;
302+ const sourceLocation = frame . sourceFile
303+ ? ` (${ path . basename ( frame . sourceFile ) } ${ typeof frame . sourceLine === "number" ? `:${ frame . sourceLine } ` : "" } )`
304+ : "" ;
305+ lines . push ( `${ String ( frameIndex ) . padStart ( 3 , " " ) } ${ imageName } ${ symbol } ${ symbolLocation } ${ sourceLocation } ` ) ;
306+ }
307+ }
308+
309+ return lines . join ( "\n" ) ;
310+ }
311+
312+ function formatBacktraceFromCrashText ( crashContent ) {
313+ const match = crashContent . match ( / T h r e a d \s + \d + \s + C r a s h e d : [ \s \S ] * ?(? = \n \n T h r e a d \s + \d + | \n B i n a r y I m a g e s : | $ ) / ) ;
314+ return match ? match [ 0 ] : null ;
315+ }
316+
317+ function emitLLDBBacktrace ( appBinaryPath , runArgs ) {
318+ const runCommand = runArgs . length > 0
319+ ? `run ${ runArgs . map ( quoteForLLDB ) . join ( " " ) } `
320+ : "run" ;
321+ const args = [
322+ "lldb" ,
323+ "--batch" ,
324+ "--one-line" , "process handle -p true -s false -n false SIGSEGV SIGBUS SIGABRT SIGILL SIGTRAP" ,
325+ "--one-line" , runCommand ,
326+ "--one-line" , "thread backtrace all" ,
327+ "--" ,
328+ appBinaryPath
329+ ] ;
330+
331+ const result = cp . spawnSync ( "xcrun" , args , {
332+ encoding : "utf8" ,
333+ timeout : commandTimeoutMs
334+ } ) ;
335+
336+ if ( result . error ) {
337+ console . error ( `ERROR: Unable to collect LLDB backtrace: ${ result . error . message } ` ) ;
338+ return ;
339+ }
340+
341+ const output = `${ result . stdout || "" } ${ result . stderr || "" } ` . trim ( ) ;
342+ if ( output . length === 0 ) {
343+ console . error ( "ERROR: LLDB produced no backtrace output." ) ;
344+ return ;
345+ }
346+
347+ console . error ( "\n--- Crash Backtrace (LLDB) ---" ) ;
348+ console . error ( output ) ;
349+ }
350+
351+ async function emitCrashBacktrace ( appBinaryPath , runArgs , launchedAtMs , pid ) {
352+ const deadline = Date . now ( ) + 5000 ;
353+
354+ while ( Date . now ( ) < deadline ) {
355+ const report = readRecentCrashReportForPid ( pid , launchedAtMs ) ;
356+ if ( report ) {
357+ const formatted = report . path . endsWith ( ".ips" )
358+ ? formatBacktraceFromIPS ( report . content )
359+ : formatBacktraceFromCrashText ( report . content ) ;
360+
361+ if ( formatted ) {
362+ console . error ( `\n--- Crash Backtrace (${ report . path } ) ---` ) ;
363+ console . error ( formatted ) ;
364+ return ;
365+ }
366+
367+ break ;
368+ }
369+
370+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) ) ;
371+ }
372+
373+ emitLLDBBacktrace ( appBinaryPath , runArgs ) ;
374+ }
375+
178376function runBuildAndRequireSuccess ( command , args , timeoutMs = commandTimeoutMs ) {
179377 const result = cp . spawnSync ( command , args , {
180378 encoding : "utf8" ,
@@ -304,6 +502,8 @@ function main() {
304502 let appLaunched = false ;
305503 let timeoutTimer = null ;
306504 let inactivityTimer = null ;
505+ let childPid = null ;
506+ let launchedAtMs = Date . now ( ) ;
307507
308508 function clearTimers ( ) {
309509 if ( timeoutTimer ) {
@@ -343,6 +543,8 @@ function main() {
343543 const child = cp . spawn ( appBinaryPath , runArgs , {
344544 stdio : [ "ignore" , "pipe" , "pipe" ]
345545 } ) ;
546+ childPid = child . pid ;
547+ launchedAtMs = Date . now ( ) ;
346548
347549 function createChunkHandler ( ) {
348550 let leftover = "" ;
@@ -406,12 +608,18 @@ function main() {
406608 failAndExit ( `ERROR: Failed to start TestRunner process: ${ error . message } ` , child ) ;
407609 } ) ;
408610
409- child . on ( "close" , ( code ) => {
611+ child . on ( "close" , async ( code , signal ) => {
410612 clearTimers ( ) ;
411613 results . end ( ) ;
614+ const exitStatus = getProcessExitStatus ( code , signal ) ;
412615
413616 if ( ! completedSuccessfully ) {
414- failAndExit ( `ERROR: Test run failed before JUnit completion. Exit code: ${ code } ` , null ) ;
617+ if ( isLikelyCrash ( code , signal ) ) {
618+ await emitCrashBacktrace ( appBinaryPath , runArgs , launchedAtMs , childPid ) ;
619+ }
620+
621+ console . error ( `ERROR: Test run failed before JUnit completion. Exit code: ${ exitStatus . display } ` ) ;
622+ process . exit ( exitStatus . code ) ;
415623 return ;
416624 }
417625
0 commit comments