also looking at this
fix(android): make every Zig decline speak, and prove Zig answers
#205
8 files
+616
-255
| @@ -2,7 +2,7 @@ import type { LegOutcome, RunnerOptions } from './types' | ||
| 2 | 2 | import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' |
| 3 | 3 | import { join } from 'node:path' |
| 4 | 4 | import { init } from '../../packages/android/src/index' |
| 5 | import { evaluateRun, hasTerminated } from './protocol' | |
| 5 | import { androidDeclines, evaluateRun, hasTerminated } from './protocol' | |
| 6 | 6 | import { command, driverPage, waitForFile } from './support' |
| 7 | 7 | |
| 8 | 8 | /** |
| @@ -189,17 +189,23 @@ async function runLeg(leg: Leg, options: RunnerOptions): Promise<LegOutcome> { | ||
| 189 | 189 | failures.push(`the bridge reported sdkVersion ${observed[1]} but the device says ${sdk}`) |
| 190 | 190 | } |
| 191 | 191 | |
| 192 | // Did the Zig runtime actually load and bind? JNI_OnLoad says so itself, on | |
| 193 | // the happy path as well as the failure ones — which is deliberate, because | |
| 194 | // "loaded and bound" and "never shipped" are otherwise both silent, and this | |
| 195 | // is also the only line that proves the log channel is working rather than | |
| 196 | // merely quiet. | |
| 192 | // Did the Zig runtime load, bind — and then actually answer? JNI_OnLoad | |
| 193 | // reports the first two on the happy path as well as the failure ones, and | |
| 194 | // every way a native can give up now says so too (android_dispatch.zig's | |
| 195 | // fellThrough / failedWithoutFallback / undelivered). | |
| 197 | 196 | // |
| 198 | // There is no per-action dispatch line to count the way the iOS legs do. | |
| 199 | // Binding is the gate instead: once the natives are registered, every | |
| 200 | // CraftNative wrapper returns a value and the Kotlin falls through to Zig | |
| 201 | // rather than the other way round. | |
| 197 | // Registration alone is not enough, and the first run that loaded this | |
| 198 | // library showed why: "registered 103 natives", every case passed, and | |
| 199 | // getDeviceInfo threw NoSuchFieldError on every call and handed it to Kotlin. | |
| 200 | // The page could not tell. So the runtime leg asks for both halves: bound, | |
| 201 | // and not one decline while the page ran. | |
| 202 | // | |
| 203 | // Read from the full buffer, not the filtered dump: the stack trace ART | |
| 204 | // prints for the underlying Java exception is under System.err, which is | |
| 205 | // the part worth quoting back. | |
| 202 | 206 | const registered = logText.match(REGISTERED) |
| 207 | const fullLog = existsSync(join(evidence, 'logcat-full.txt')) ? readFileSync(join(evidence, 'logcat-full.txt'), 'utf8') : logText | |
| 208 | const declines = androidDeclines(fullLog) | |
| 203 | 209 | const zigActions: string[] = [] |
| 204 | 210 | |
| 205 | 211 | if (leg.requireZig) { |
| @@ -212,6 +218,14 @@ async function runLeg(leg: Leg, options: RunnerOptions): Promise<LegOutcome> { | ||
| 212 | 218 | else { |
| 213 | 219 | zigActions.push(`registered:${registered[1]}`) |
| 214 | 220 | } |
| 221 | ||
| 222 | for (const decline of declines) { | |
| 223 | failures.push( | |
| 224 | `Zig declined while the page ran — ${decline}. The case still passed because Kotlin answered instead, ` | |
| 225 | + `which is exactly what this leg exists to see; the Java exception, if any, is under System.err in ${label}/logcat-full.txt`, | |
| 226 | ) | |
| 227 | } | |
| 228 | if (registered && !declines.length) zigActions.push('declines:0') | |
| 215 | 229 | } |
| 216 | 230 | else if (registered) { |
| 217 | 231 | failures.push(`the shim leg registered ${registered[1]} natives; it was generated with no runtime`) |