Skip to content
Open
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
62 changes: 61 additions & 1 deletion .github/actions/assemble-test-project/default-smoke-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,63 @@
// file the action drops next to this file at CI time, so this template is
// a plain, runnable file — no placeholder substitution.

const fs = require('fs')

const { moduleName } = require('./harness-config.json')

// `err.stack` omits `cause`, so print the chain.
function errorChain(err) {
const lines = []
let e = err
for (let depth = 0; e && depth < 5; depth++) {
if (depth > 0) lines.push('caused by:')
for (const line of String(e.stack || e).split('\n')) lines.push(line)
e = e.cause
}
return lines
}

// require-addon raises "Cannot find addon" when every candidate fails to
// *load* — not only when none exist. A prebuild that is present but can't
// dlopen (built without linking libnode.so so napi_* never resolves, wrong
// ABI, …) therefore reports exactly like a missing file, which is precisely
// the failure this smoke test exists to catch. Its `cause` doesn't settle it
// either: the resolver overwrites `cause` per candidate, so it ends up holding
// the last candidate's error, normally a "Cannot find module" for a path that
// never existed.
//
// The candidate list is on the error, so retry the ones actually on disk and
// report why each failed. That separates "prebuild wasn't installed for this
// target" from "prebuild is installed but broken".
function addonDiagnostics(err) {
if (!err || err.code !== 'ADDON_NOT_FOUND') return []
if (!Array.isArray(err.candidates)) return []

const { fileURLToPath } = require('url')
const lines = []

for (const candidate of err.candidates) {
let file
try {
file = fileURLToPath(candidate)
} catch {
continue // non-file: candidate (e.g. `linked:`)
}
if (!fs.existsSync(file)) continue
try {
process.dlopen({ exports: {} }, file)
lines.push(file + ': loaded on retry (original failure was elsewhere)')
} catch (e) {
lines.push(file + ': ' + (e && e.message ? e.message : e))
}
}

if (lines.length === 0) {
return ['no candidate exists on disk — no prebuild was installed for this target']
}
return ['candidate(s) present but failed to load:'].concat(lines)
}

try {
require(moduleName)
console.log('TAP version 13')
Expand All @@ -17,8 +72,13 @@ try {
console.log('not ok 1 - require(' + moduleName + ') threw')
console.log(' ---')
console.log(' message: ' + JSON.stringify(err && err.message))
const diagnostics = addonDiagnostics(err)
if (diagnostics.length > 0) {
console.log(' addon_diagnostics: |')
for (const line of diagnostics) console.log(' ' + line)
}
console.log(' stack: |')
for (const line of String((err && err.stack) || err).split('\n')) {
for (const line of errorChain(err)) {
console.log(' ' + line)
}
console.log(' ...')
Expand Down
56 changes: 55 additions & 1 deletion .github/actions/assemble-test-project/module-tests-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,62 @@ async function main() {
}
}

// `err.stack` omits `cause`, so print the chain.
function formatError(err) {
const parts = []
let e = err
for (let depth = 0; e && depth < 5; depth++) {
parts.push((depth === 0 ? '' : 'caused by: ') + String(e.stack || e))
e = e.cause
}
return parts.join('\n')
}

// require-addon raises "Cannot find addon" when every candidate fails to
// *load* — not only when none exist. A prebuild that is present but can't
// dlopen (built without linking libnode.so so napi_* never resolves, wrong
// ABI, …) therefore reports exactly like a missing file. Its `cause` doesn't
// settle it either: the resolver overwrites `cause` per candidate, so it ends
// up holding the last candidate's error, normally a "Cannot find module" for a
// path that never existed.
//
// The candidate list is on the error, so retry the ones actually on disk and
// report why each failed. That separates "prebuild wasn't installed for this
// target" from "prebuild is installed but broken".
function addonDiagnostics(err) {
if (!err || err.code !== 'ADDON_NOT_FOUND') return []
if (!Array.isArray(err.candidates)) return []

const { fileURLToPath } = require('url')
const lines = []

for (const candidate of err.candidates) {
let file
try {
file = fileURLToPath(candidate)
} catch {
continue // non-file: candidate (e.g. `linked:`)
}
if (!fs.existsSync(file)) continue
try {
process.dlopen({ exports: {} }, file)
lines.push(' ' + file + ': loaded on retry (original failure was elsewhere)')
} catch (e) {
lines.push(' ' + file + ': ' + (e && e.message ? e.message : e))
}
}

if (lines.length === 0) {
return [
'Addon diagnostics: no candidate exists on disk — no prebuild was installed for this target.'
]
}
return ['Addon diagnostics: candidate(s) present but failed to load:'].concat(lines)
}

main().catch((err) => {
console.error('Fatal error loading tests from ' + moduleName + ':')
console.error(err && err.stack ? err.stack : err)
console.error(formatError(err))
for (const line of addonDiagnostics(err)) console.error(line)
process.exit(1)
})
15 changes: 14 additions & 1 deletion test-harness/android/run-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,20 @@ adb logcat -c
# notices the pipe has closed when it next tries to write, and no further
# lines are coming once the app has exited.) Started BEFORE the launch so
# no early NODEJS-MOBILE output is missed.
coproc LOGCAT { adb logcat -v raw -s NODEJS-MOBILE:V; }
#
# Tags beyond NODEJS-MOBILE (node's stdout/stderr, pumped by native-lib.cpp)
# carry failures that are otherwise invisible here, which makes a broken run
# look like silence:
# nodejs node's own fatal-error reports, logged by nodejs-mobile's
# libnode rather than through our stdout/stderr pipe — this is
# where an uncaught exception's message actually lands.
# System.err Java stack traces. TestActivity's asset copy catches and
# prints per-file failures, so a file missing on device (and a
# resulting "cannot find addon") is only explained here.
# AndroidRuntime uncaught Java exceptions killing the app before node starts.
# `-v raw` is kept so TAP output stays clean and the sentinel parsing below is
# unchanged; extra tags interleave without a prefix.
coproc LOGCAT { adb logcat -v raw -s NODEJS-MOBILE:V nodejs:V System.err:W AndroidRuntime:E; }

# Launch the activity. The app pumps node's stdout/stderr to logcat tag
# NODEJS-MOBILE and emits __NODE_EXIT__:<code> when done. Fire-and-forget,
Expand Down