| @@ -62,12 +62,37 @@ pub fn clearVm() void { |
| 62 | 62 | /// being diagnosed. |
| 63 | 63 | var dropped: usize = 0; |
| 64 | 64 | |
| 65 | +/// Whether the first drop has been said out loud. |
| 66 | +/// |
| 67 | +/// Counting alone left the whole thing invisible: nothing reads `dropped` on a |
| 68 | +/// device, and the runtime E2E leg scans the log. So the first drop of a |
| 69 | +/// process logs, in the same words the other three decline paths use, and the |
| 70 | +/// rest are counted (#231). One line cannot drown a diagnosis; silence hid a |
| 71 | +/// script nobody delivered behind one that arrived. |
| 72 | +var drop_announced = false; |
| 73 | + |
| 65 | 74 | pub fn droppedCount() usize { |
| 66 | 75 | return dropped; |
| 67 | 76 | } |
| 68 | 77 | |
| 69 | 78 | pub fn resetDroppedForTest() void { |
| 70 | 79 | dropped = 0; |
| 80 | + drop_announced = false; |
| 81 | +} |
| 82 | + |
| 83 | +/// Count a script that never reached the page, and say so the first time. |
| 84 | +fn dropScript(reason: []const u8) void { |
| 85 | + dropped += 1; |
| 86 | + if (drop_announced) return; |
| 87 | + drop_announced = true; |
| 88 | + // `warn`, the level `fellThrough` uses, for the same reason: a drop before |
| 89 | + // the WebView exists is ordinary, and one after it is not, and this side |
| 90 | + // cannot tell which it is looking at. The E2E scan matches the phrase, not |
| 91 | + // the level, and fails the runtime leg on either. |
| 92 | + std.log.warn( |
| 93 | + "craft: deliver could not reach the page ({s}); further drops are counted, not logged", |
| 94 | + .{reason}, |
| 95 | + ); |
| 71 | 96 | } |
| 72 | 97 | |
| 73 | 98 | /// Hand `script` to `CraftNative.deliver`, from any thread. |
| @@ -77,18 +102,18 @@ pub fn resetDroppedForTest() void { |
| 77 | 102 | /// or an error nobody reads. `droppedCount` is what a diagnosis reads instead. |
| 78 | 103 | pub fn evaluate(allocator: std.mem.Allocator, script: []const u8) void { |
| 79 | 104 | const vm = java_vm orelse { |
| 80 | | - dropped += 1; |
| 105 | + dropScript("NoVm"); |
| 81 | 106 | return; |
| 82 | 107 | }; |
| 83 | 108 | |
| 84 | 109 | const attachment = jni.attachCurrentThread(vm) orelse { |
| 85 | | - dropped += 1; |
| 110 | + dropScript("NotAttached"); |
| 86 | 111 | return; |
| 87 | 112 | }; |
| 88 | 113 | defer attachment.release(vm); |
| 89 | 114 | |
| 90 | | - deliverThrough(allocator, attachment.env, script) catch { |
| 91 | | - dropped += 1; |
| 115 | + deliverThrough(allocator, attachment.env, script) catch |err| { |
| 116 | + dropScript(@errorName(err)); |
| 92 | 117 | }; |
| 93 | 118 | } |
| 94 | 119 | |
| @@ -109,8 +134,12 @@ fn deliverThrough(allocator: std.mem.Allocator, env: jni.JNIEnv, script: []const |
| 109 | 134 | // halves rather than one four-byte sequence. |
| 110 | 135 | const text = try j.newStringUtf8(allocator, script); |
| 111 | 136 | |
| 112 | | - const deliver = try j.staticMethodId(cls, "deliver", "(Ljava/lang/String;)V"); |
| 113 | | - try j.callStaticVoidMethodA(cls, deliver, &.{.{ .l = text }}); |
| 137 | + // `Z`, not `V`: the answer is whether a deliverer took the script. It |
| 138 | + // returned void until #231, so a script handed over before the WebView |
| 139 | + // existed was dropped by Kotlin and reported as delivered here. |
| 140 | + const deliver = try j.staticMethodId(cls, "deliver", "(Ljava/lang/String;)Z"); |
| 141 | + const took = try j.callStaticBooleanMethodA(cls, deliver, &.{.{ .l = text }}); |
| 142 | + if (took == jni.JNI_FALSE) return error.NoDeliverer; |
| 114 | 143 | } |
| 115 | 144 | |
| 116 | 145 | /// `window.dispatchEvent(new CustomEvent('name', {detail: json}))`. |
| @@ -185,6 +214,7 @@ var fake_detach_calls: usize = 0; |
| 185 | 214 | var fake_already_attached = true; |
| 186 | 215 | var fake_attach_succeeds = true; |
| 187 | 216 | var fake_find_class_succeeds = true; |
| 217 | +var fake_deliverer_installed = true; |
| 188 | 218 | |
| 189 | 219 | fn eobj(tag: usize) jni.jobject { |
| 190 | 220 | return @ptrCast(&fake_storage[tag]); |
| @@ -218,7 +248,9 @@ fn eNewStringUTF(_: jni.JNIEnv, text: [*:0]const u8) callconv(.c) jni.jstring { |
| 218 | 248 | @memcpy(fake_delivered[0..fake_delivered_len], span[0..fake_delivered_len]); |
| 219 | 249 | return eobj(2); |
| 220 | 250 | } |
| 221 | | -fn eCallStaticVoidMethodA(_: jni.JNIEnv, _: jni.jclass, _: jni.jmethodID, _: [*]const jni.jvalue) callconv(.c) void {} |
| 251 | +fn eCallStaticBooleanMethodA(_: jni.JNIEnv, _: jni.jclass, _: jni.jmethodID, _: [*]const jni.jvalue) callconv(.c) jni.jboolean { |
| 252 | + return if (fake_deliverer_installed) jni.JNI_TRUE else jni.JNI_FALSE; |
| 253 | +} |
| 222 | 254 | fn eExceptionOccurred(_: jni.JNIEnv) callconv(.c) jni.jobject { |
| 223 | 255 | return null; |
| 224 | 256 | } |
| @@ -234,7 +266,7 @@ fn fakeVm(invoke: *jni.JNIInvokeInterface) void { |
| 234 | 266 | fake_env_table.FindClass = @ptrCast(&eFindClass); |
| 235 | 267 | fake_env_table.GetStaticMethodID = @ptrCast(&eStaticMethodId); |
| 236 | 268 | fake_env_table.NewStringUTF = @ptrCast(&eNewStringUTF); |
| 237 | | - fake_env_table.CallStaticVoidMethodA = @ptrCast(&eCallStaticVoidMethodA); |
| 269 | + fake_env_table.CallStaticBooleanMethodA = @ptrCast(&eCallStaticBooleanMethodA); |
| 238 | 270 | fake_env_table.ExceptionOccurred = @ptrCast(&eExceptionOccurred); |
| 239 | 271 | fake_env_table.PushLocalFrame = @ptrCast(&ePush); |
| 240 | 272 | fake_env_table.PopLocalFrame = @ptrCast(&ePop); |
| @@ -251,6 +283,7 @@ fn fakeVm(invoke: *jni.JNIInvokeInterface) void { |
| 251 | 283 | fake_already_attached = true; |
| 252 | 284 | fake_attach_succeeds = true; |
| 253 | 285 | fake_find_class_succeeds = true; |
| 286 | + fake_deliverer_installed = true; |
| 254 | 287 | resetDroppedForTest(); |
| 255 | 288 | } |
| 256 | 289 | |
| @@ -293,7 +326,7 @@ test "a detached thread is attached and then detached again" { |
| 293 | 326 | try testing.expectEqual(@as(usize, 1), fake_detach_calls); |
| 294 | 327 | } |
| 295 | 328 | |
| 296 | | -test "every way delivery can fail is counted, not logged and not crashed" { |
| 329 | +test "every way delivery can fail is counted, said once, and not crashed" { |
| 297 | 330 | var invoke: jni.JNIInvokeInterface = undefined; |
| 298 | 331 | fakeVm(&invoke); |
| 299 | 332 | const ptr: *const jni.JNIInvokeInterface = &invoke; |
| @@ -322,6 +355,38 @@ test "every way delivery can fail is counted, not logged and not crashed" { |
| 322 | 355 | resetDroppedForTest(); |
| 323 | 356 | evaluate(testing.allocator, "window.z = 3;"); |
| 324 | 357 | try testing.expectEqual(@as(usize, 1), droppedCount()); |
| 358 | + |
| 359 | + // #231: Kotlin has no deliverer, which is every moment before the WebView |
| 360 | + // exists. `deliver` returned void, so this arrived here as a success and |
| 361 | + // the script was gone — the page waiting on a promise nothing settles. |
| 362 | + fake_find_class_succeeds = true; |
| 363 | + fake_deliverer_installed = false; |
| 364 | + resetDroppedForTest(); |
| 365 | + evaluate(testing.allocator, "window.z = 3;"); |
| 366 | + try testing.expectEqual(@as(usize, 1), droppedCount()); |
| 367 | +} |
| 368 | + |
| 369 | +test "a run of drops is announced once and counted the rest of the way" { |
| 370 | + // The count exists because a device that drops drops in bulk, and the |
| 371 | + // announcement exists because nothing on a device reads the count. Both, |
| 372 | + // or the drop is either invisible or drowns what it is being read for. |
| 373 | + var invoke: jni.JNIInvokeInterface = undefined; |
| 374 | + fakeVm(&invoke); |
| 375 | + const ptr: *const jni.JNIInvokeInterface = &invoke; |
| 376 | + setVm(&ptr); |
| 377 | + defer clearVm(); |
| 378 | + |
| 379 | + fake_deliverer_installed = false; |
| 380 | + for (0..5) |_| evaluate(testing.allocator, "window.z = 3;"); |
| 381 | + |
| 382 | + try testing.expectEqual(@as(usize, 5), droppedCount()); |
| 383 | + try testing.expect(drop_announced); |
| 384 | + |
| 385 | + // And delivery resumes without the counter moving. |
| 386 | + fake_deliverer_installed = true; |
| 387 | + evaluate(testing.allocator, "window.ok = 1;"); |
| 388 | + try testing.expectEqual(@as(usize, 5), droppedCount()); |
| 389 | + try testing.expectEqualStrings("window.ok = 1;", delivered()); |
| 325 | 390 | } |
| 326 | 391 | |
| 327 | 392 | test "an attached thread is detached even when delivery fails" { |