Skip to content

Commit cd12261

Browse files
committed
feat: enable additional test modules and enhance crash reporting in macOS test runner
1 parent 7b4ad3d commit cd12261

2 files changed

Lines changed: 214 additions & 6 deletions

File tree

TestRunner/app/tests/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,11 @@ loadTest("./Marshalling/EnumTests");
195195
loadTest("./Marshalling/ProtocolTests");
196196
//
197197
// import "./Inheritance/ConstructorResolutionTests";
198-
// require("./Inheritance/InheritanceTests");
198+
require("./Inheritance/InheritanceTests");
199199
loadTest("./Inheritance/ProtocolImplementationTests");
200200
loadTest("./Inheritance/TypeScriptTests");
201201
//
202-
// require("./MethodCallsTests");
202+
require("./MethodCallsTests");
203203
//import "./FunctionsTests";
204204
loadTest("./VersionDiffTests");
205205
loadTest("./ObjCConstructors");
@@ -216,9 +216,9 @@ loadTest("./RuntimeImplementedAPIs");
216216

217217
// require("./Timers");
218218

219-
// require("./URL");
219+
require("./URL");
220220
loadTest("./URLSearchParams");
221-
// loadTest("./URLPattern");
221+
loadTest("./URLPattern");
222222

223223
// Exception handling tests
224224
loadTest("./ExceptionHandlingTests");

scripts/run-tests-macos.js

Lines changed: 210 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const fs = require("fs");
1515
const path = require("path");
1616
const cp = require("child_process");
1717
const crypto = require("crypto");
18+
const os = require("os");
1819

1920
const projectPath = path.join(__dirname, "../napi-ios.xcodeproj");
2021
const scheme = "TestRunner";
@@ -57,6 +58,7 @@ const launchedMarker = "Application Start!";
5758
const junitPrefix = "TKUnit: ";
5859
const junitEndTag = "</testsuites>";
5960
const consoleLogMarker = "CONSOLE LOG:";
61+
const crashReportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports");
6062

6163
function 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(/Thread\s+\d+\s+Crashed:[\s\S]*?(?=\n\nThread\s+\d+|\nBinary Images:|$)/);
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+
178376
function 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

Comments
 (0)