From 3cdf2b05be48657edd7df394d36fbf3092a185a4 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 26 Aug 2026 10:35:17 -0700 Subject: [PATCH 1/3] Pump V8's foreground task queue so async WebAssembly settles AppRuntime_V8 creates a default v8::Platform but nothing ever called v8::platform::PumpMessageLoop. V8 finishes asynchronous WebAssembly compilation on a background thread and then posts a *foreground* task to settle the promise on the isolate thread, so WebAssembly.compile, instantiate and instantiateStreaming never resolved or rejected - any Emscripten module hung forever. Sync `new WebAssembly.Module` was unaffected, which made this look like a hang rather than a failure. AppRuntime_JSI already did the equivalent via TaskRunnerAdapter; the direct-V8 path was simply missing it. DrainMicrotasks now pumps the queue with kDoNotWait, so it never blocks the JavaScript thread. Because that only runs after a dispatched callback, the platform is also wrapped in a DispatchingPlatform whose foreground task runner nudges the app dispatcher, giving a pump when the app is otherwise idle (no render loop, no timers). The wrapper leaves the default platform owning the queue so task ordering, nestability and delays keep V8's own semantics, and the wake is coalesced through an atomic flag so a burst of posted tasks cannot flood the dispatcher. The three new tests time out without this change. --- Core/AppRuntime/Source/AppRuntime_V8.cpp | 216 ++++++++++++++++++++++- Tests/UnitTests/Scripts/tests.ts | 36 ++++ 2 files changed, 247 insertions(+), 5 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime_V8.cpp b/Core/AppRuntime/Source/AppRuntime_V8.cpp index 89928dcf..f9547c6b 100644 --- a/Core/AppRuntime/Source/AppRuntime_V8.cpp +++ b/Core/AppRuntime/Source/AppRuntime_V8.cpp @@ -7,12 +7,173 @@ #include #endif +#include +#include +#include +#include +#include #include +#include namespace Babylon { namespace { + // V8 hands work that finishes off-thread - most visibly asynchronous WebAssembly + // compilation - back to the isolate by posting a task to the platform's *foreground* + // task runner. Those tasks only run when someone calls v8::platform::PumpMessageLoop, + // and the host's JavaScript thread sits in a blocking dispatcher wait, so nothing + // would ever pump it: WebAssembly.compile/instantiate simply never settled, and + // anything built on an Emscripten module hung forever. + // + // This platform wraps the default one and leaves it owning the queue (so task + // ordering, nestability and delays keep V8's own semantics). All it adds is a + // wake-up: whenever V8 posts foreground work, the AppRuntime dispatcher is nudged, + // and the pump in AppRuntime::DrainMicrotasks then drains the queue on the + // JavaScript thread. + class DispatchingPlatform final : public v8::Platform + { + public: + explicit DispatchingPlatform(std::unique_ptr inner) + : m_inner{std::move(inner)} + { + } + + v8::Platform& Inner() + { + return *m_inner; + } + + void SetWake(v8::Isolate* isolate, std::function wake) + { + std::scoped_lock lock{m_mutex}; + if (wake) + { + m_wakes[isolate] = std::move(wake); + } + else + { + m_wakes.erase(isolate); + m_taskRunners.erase(isolate); + } + } + + void Wake(v8::Isolate* isolate) + { + std::function wake; + { + std::scoped_lock lock{m_mutex}; + const auto entry = m_wakes.find(isolate); + if (entry == m_wakes.end()) + { + return; + } + wake = entry->second; + } + wake(); + } + + v8::PageAllocator* GetPageAllocator() override { return m_inner->GetPageAllocator(); } + v8::ThreadIsolatedAllocator* GetThreadIsolatedAllocator() override { return m_inner->GetThreadIsolatedAllocator(); } + v8::ZoneBackingAllocator* GetZoneBackingAllocator() override { return m_inner->GetZoneBackingAllocator(); } + void OnCriticalMemoryPressure() override { m_inner->OnCriticalMemoryPressure(); } + int NumberOfWorkerThreads() override { return m_inner->NumberOfWorkerThreads(); } + + std::shared_ptr GetForegroundTaskRunner(v8::Isolate* isolate) override + { + return GetForegroundTaskRunner(isolate, v8::TaskPriority::kUserBlocking); + } + + std::shared_ptr GetForegroundTaskRunner(v8::Isolate* isolate, v8::TaskPriority priority) override; + + void CallOnWorkerThread(std::unique_ptr task) override { m_inner->CallOnWorkerThread(std::move(task)); } + void CallBlockingTaskOnWorkerThread(std::unique_ptr task) override { m_inner->CallBlockingTaskOnWorkerThread(std::move(task)); } + void CallLowPriorityTaskOnWorkerThread(std::unique_ptr task) override { m_inner->CallLowPriorityTaskOnWorkerThread(std::move(task)); } + void CallDelayedOnWorkerThread(std::unique_ptr task, double delayInSeconds) override { m_inner->CallDelayedOnWorkerThread(std::move(task), delayInSeconds); } + bool IdleTasksEnabled(v8::Isolate* isolate) override { return m_inner->IdleTasksEnabled(isolate); } + std::unique_ptr PostJob(v8::TaskPriority priority, std::unique_ptr jobTask) override { return m_inner->PostJob(priority, std::move(jobTask)); } + std::unique_ptr CreateJob(v8::TaskPriority priority, std::unique_ptr jobTask) override { return m_inner->CreateJob(priority, std::move(jobTask)); } + std::unique_ptr CreateBlockingScope(v8::BlockingType blockingType) override { return m_inner->CreateBlockingScope(blockingType); } + double MonotonicallyIncreasingTime() override { return m_inner->MonotonicallyIncreasingTime(); } + int64_t CurrentClockTimeMilliseconds() override { return m_inner->CurrentClockTimeMilliseconds(); } + double CurrentClockTimeMillis() override { return m_inner->CurrentClockTimeMillis(); } + double CurrentClockTimeMillisecondsHighResolution() override { return m_inner->CurrentClockTimeMillisecondsHighResolution(); } + StackTracePrinter GetStackTracePrinter() override { return m_inner->GetStackTracePrinter(); } + v8::TracingController* GetTracingController() override { return m_inner->GetTracingController(); } + void DumpWithoutCrashing() override { m_inner->DumpWithoutCrashing(); } + v8::HighAllocationThroughputObserver* GetHighAllocationThroughputObserver() override { return m_inner->GetHighAllocationThroughputObserver(); } + + private: + std::unique_ptr m_inner; + std::mutex m_mutex; + std::map> m_wakes; + std::map> m_taskRunners; + }; + + class WakingTaskRunner final : public v8::TaskRunner + { + public: + WakingTaskRunner(std::shared_ptr inner, DispatchingPlatform& platform, v8::Isolate* isolate) + : m_inner{std::move(inner)} + , m_platform{platform} + , m_isolate{isolate} + { + } + + void PostTask(std::unique_ptr task) override + { + m_inner->PostTask(std::move(task)); + m_platform.Wake(m_isolate); + } + + void PostNonNestableTask(std::unique_ptr task) override + { + m_inner->PostNonNestableTask(std::move(task)); + m_platform.Wake(m_isolate); + } + + void PostDelayedTask(std::unique_ptr task, double delayInSeconds) override + { + m_inner->PostDelayedTask(std::move(task), delayInSeconds); + m_platform.Wake(m_isolate); + } + + void PostNonNestableDelayedTask(std::unique_ptr task, double delayInSeconds) override + { + m_inner->PostNonNestableDelayedTask(std::move(task), delayInSeconds); + m_platform.Wake(m_isolate); + } + + void PostIdleTask(std::unique_ptr task) override + { + m_inner->PostIdleTask(std::move(task)); + } + + bool IdleTasksEnabled() override { return m_inner->IdleTasksEnabled(); } + bool NonNestableTasksEnabled() const override { return m_inner->NonNestableTasksEnabled(); } + bool NonNestableDelayedTasksEnabled() const override { return m_inner->NonNestableDelayedTasksEnabled(); } + + private: + std::shared_ptr m_inner; + DispatchingPlatform& m_platform; + v8::Isolate* m_isolate; + }; + + std::shared_ptr DispatchingPlatform::GetForegroundTaskRunner(v8::Isolate* isolate, v8::TaskPriority) + { + // The priority-aware overload is not implemented by libplatform's DefaultPlatform in + // this V8 version - the base class default returns nullptr - so always ask the inner + // platform for the plain per-isolate runner. The wrapper is cached because V8 calls + // this often and hands the result around by pointer. + std::scoped_lock lock{m_mutex}; + auto& runner = m_taskRunners[isolate]; + if (!runner) + { + runner = std::make_shared(m_inner->GetForegroundTaskRunner(isolate), *this, isolate); + } + return runner; + } + class Module final { public: @@ -20,7 +181,7 @@ namespace Babylon { v8::V8::InitializeICUDefaultLocation(executablePath); v8::V8::InitializeExternalStartupData(executablePath); - m_platform = v8::platform::NewDefaultPlatform(); + m_platform = std::make_unique(v8::platform::NewDefaultPlatform()); v8::V8::InitializePlatform(m_platform.get()); v8::V8::Initialize(); } @@ -49,13 +210,25 @@ namespace Babylon return *s_module; } - v8::Platform& Platform() + static Module* TryInstance() + { + return s_module.get(); + } + + DispatchingPlatform& Platform() { return *m_platform; } + // v8::platform::PumpMessageLoop downcasts to the libplatform DefaultPlatform, so it + // has to be handed the wrapped platform rather than the wrapper. + v8::Platform& DefaultPlatform() + { + return m_platform->Inner(); + } + private: - std::unique_ptr m_platform; + std::unique_ptr m_platform; static std::unique_ptr s_module; }; @@ -72,6 +245,20 @@ namespace Babylon create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); v8::Isolate* isolate = v8::Isolate::New(create_params); + // Nudge the dispatcher whenever V8 posts foreground work for this isolate, so the + // pump in DrainMicrotasks gets a chance to run it even when the app is otherwise + // idle (no render loop, no timers). The flag collapses bursts of posts into a single + // pending wake-up; it is cleared once that wake-up has been serviced. + auto wakePending = std::make_shared(false); + Module::Instance().Platform().SetWake(isolate, [this, wakePending]() { + if (wakePending->exchange(true)) + { + return; + } + + Dispatch([wakePending](Napi::Env) { wakePending->store(false); }); + }); + // Use the isolate within a scope. { v8::Isolate::Scope isolate_scope{isolate}; @@ -108,6 +295,8 @@ namespace Babylon } // Destroy the isolate. + Module::Instance().Platform().SetWake(isolate, nullptr); + // todo : GetArrayBufferAllocator not available? // delete isolate->GetArrayBufferAllocator(); isolate->Dispose(); @@ -115,7 +304,24 @@ namespace Babylon void AppRuntime::DrainMicrotasks(Napi::Env) { - // V8 auto-drains microtasks at the end of each script/callback when - // using the default MicrotasksPolicy. No explicit pump needed. + // V8 auto-drains microtasks at the end of each script/callback when using the default + // MicrotasksPolicy, but microtasks are not the whole story: work that V8 hands to the + // v8::Platform completes on a background thread and then posts a *foreground* task to + // settle its result on the isolate thread. Asynchronous WebAssembly compilation is the + // visible case - WebAssembly.compile/instantiate/instantiateStreaming would never + // resolve or reject, hanging any Emscripten module (and therefore anything built on + // one) forever. Nothing else in the host drains that queue, so pump it here, after + // every dispatched callback, which for a rendering app means at least once a frame. + Module* module{Module::TryInstance()}; + v8::Isolate* isolate{v8::Isolate::GetCurrent()}; + if (module == nullptr || isolate == nullptr) + { + return; + } + + // kDoNotWait: never block the JavaScript thread waiting for background work. + while (v8::platform::PumpMessageLoop(&module->DefaultPlatform(), isolate, v8::platform::MessageLoopBehavior::kDoNotWait)) + { + } } } diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 1dc2aa4c..4255fd5f 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -2148,6 +2148,42 @@ describe("FileReader", function () { }); }); +describe("WebAssembly", function () { + this.timeout(30000); + + // Minimal valid module: the 8-byte header (magic + version) and no sections. + const emptyModule = new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]); + + it("should settle the promise returned by WebAssembly.compile", async function () { + if (typeof WebAssembly === "undefined") { + this.skip(); + } + const module = await WebAssembly.compile(emptyModule); + expect(module).to.be.an.instanceof(WebAssembly.Module); + }); + + it("should settle the promise returned by WebAssembly.instantiate", async function () { + if (typeof WebAssembly === "undefined") { + this.skip(); + } + const result = await WebAssembly.instantiate(emptyModule); + expect(result.instance).to.be.an.instanceof(WebAssembly.Instance); + }); + + it("should reject the promise returned by WebAssembly.compile for invalid bytes", async function () { + if (typeof WebAssembly === "undefined") { + this.skip(); + } + let threw = false; + try { + await WebAssembly.compile(new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0xFF])); + } catch (e) { + threw = true; + } + expect(threw).to.equal(true); + }); +}); + function runTests() { mocha.run((failures: number) => { // Test program will wait for code to be set before exiting From b7fc69ecdd8f966f2e3b979fd6f0be824a1bfbb4 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 26 Aug 2026 10:53:01 -0700 Subject: [PATCH 2/3] Document why the wake flag is cleared before pumping Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- Core/AppRuntime/Source/AppRuntime_V8.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Core/AppRuntime/Source/AppRuntime_V8.cpp b/Core/AppRuntime/Source/AppRuntime_V8.cpp index f9547c6b..bb106c92 100644 --- a/Core/AppRuntime/Source/AppRuntime_V8.cpp +++ b/Core/AppRuntime/Source/AppRuntime_V8.cpp @@ -248,7 +248,14 @@ namespace Babylon // Nudge the dispatcher whenever V8 posts foreground work for this isolate, so the // pump in DrainMicrotasks gets a chance to run it even when the app is otherwise // idle (no render loop, no timers). The flag collapses bursts of posts into a single - // pending wake-up; it is cleared once that wake-up has been serviced. + // pending wake-up. + // + // The flag must be cleared in the dispatched callback, which Dispatch runs *before* + // DrainMicrotasks pumps. Clearing it afterwards instead would lose wake-ups: a task + // posted between the pump draining the queue and the flag being cleared would find + // the flag still set, skip the dispatch, and then sit in the queue with nothing + // scheduled to pump it. Clearing first can only ever cost one redundant no-op + // dispatch, because such a post is already covered by the pump that follows. auto wakePending = std::make_shared(false); Module::Instance().Platform().SetWake(isolate, [this, wakePending]() { if (wakePending->exchange(true)) From 4a5d7226a8c999c8afb9fa83da28fc1a20923454 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 26 Aug 2026 12:59:51 -0700 Subject: [PATCH 3/3] Fix the Android V8 build and scope the WebAssembly tests to V8 CI caught two problems the local V8 build could not. Android builds against V8 11.0 while desktop uses 11.9, and DispatchingPlatform forwarded the whole v8::Platform interface, including five members that do not exist in 11.0 (ThreadIsolatedAllocator, CreateBlockingScope, CurrentClockTimeMilliseconds, its high-resolution variant, and the TaskPriority overload of GetForegroundTaskRunner). Overriding a method the base class does not declare is a hard error, so those are now version-gated and the two GetForegroundTaskRunner overloads share a helper. All nine of 11.0's pure virtuals are still overridden; the gated-out members fall back to v8::Platform's own defaults, which are benign for each of them. The WebAssembly tests also ran on every engine, but the fix is V8-only, so JSC/Chakra/Hermes/QuickJS hit three 30s timeouts instead of failing fast. Expose the configured engine as a hostEngine global, mirroring hostPlatform, and skip the suite off V8. Chakra: 217 passing, 3 pending, 11.7s (was ~90s of timeouts). V8: 220 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- Core/AppRuntime/Source/AppRuntime_V8.cpp | 39 +++++++++++++++---- .../Android/app/src/main/cpp/CMakeLists.txt | 1 + Tests/UnitTests/CMakeLists.txt | 1 + Tests/UnitTests/Scripts/tests.ts | 19 ++++----- Tests/UnitTests/Shared/Shared.cpp | 1 + 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime_V8.cpp b/Core/AppRuntime/Source/AppRuntime_V8.cpp index bb106c92..cd13332e 100644 --- a/Core/AppRuntime/Source/AppRuntime_V8.cpp +++ b/Core/AppRuntime/Source/AppRuntime_V8.cpp @@ -2,6 +2,16 @@ #include #include +#include + +// Android builds against V8 11.0, desktop against 11.9. A few v8::Platform members below do not +// exist in 11.0, and overriding a method the base class does not declare is a hard error, so they +// are gated. Where a member is gated out, v8::Platform's own default is used instead of forwarding +// to the inner platform; all of them are optional hooks whose defaults are benign (null allocator, +// null blocking scope, clock values derived from CurrentClockTimeMillis). +#define JSRH_V8_AT_LEAST(major, minor) \ + (V8_MAJOR_VERSION > (major) || (V8_MAJOR_VERSION == (major) && V8_MINOR_VERSION >= (minor))) + #ifdef ENABLE_V8_INSPECTOR #include @@ -74,17 +84,24 @@ namespace Babylon } v8::PageAllocator* GetPageAllocator() override { return m_inner->GetPageAllocator(); } +#if JSRH_V8_AT_LEAST(11, 9) v8::ThreadIsolatedAllocator* GetThreadIsolatedAllocator() override { return m_inner->GetThreadIsolatedAllocator(); } +#endif v8::ZoneBackingAllocator* GetZoneBackingAllocator() override { return m_inner->GetZoneBackingAllocator(); } void OnCriticalMemoryPressure() override { m_inner->OnCriticalMemoryPressure(); } int NumberOfWorkerThreads() override { return m_inner->NumberOfWorkerThreads(); } std::shared_ptr GetForegroundTaskRunner(v8::Isolate* isolate) override { - return GetForegroundTaskRunner(isolate, v8::TaskPriority::kUserBlocking); + return WrapForegroundTaskRunner(isolate); } - std::shared_ptr GetForegroundTaskRunner(v8::Isolate* isolate, v8::TaskPriority priority) override; +#if JSRH_V8_AT_LEAST(11, 9) + std::shared_ptr GetForegroundTaskRunner(v8::Isolate* isolate, v8::TaskPriority) override + { + return WrapForegroundTaskRunner(isolate); + } +#endif void CallOnWorkerThread(std::unique_ptr task) override { m_inner->CallOnWorkerThread(std::move(task)); } void CallBlockingTaskOnWorkerThread(std::unique_ptr task) override { m_inner->CallBlockingTaskOnWorkerThread(std::move(task)); } @@ -93,17 +110,25 @@ namespace Babylon bool IdleTasksEnabled(v8::Isolate* isolate) override { return m_inner->IdleTasksEnabled(isolate); } std::unique_ptr PostJob(v8::TaskPriority priority, std::unique_ptr jobTask) override { return m_inner->PostJob(priority, std::move(jobTask)); } std::unique_ptr CreateJob(v8::TaskPriority priority, std::unique_ptr jobTask) override { return m_inner->CreateJob(priority, std::move(jobTask)); } +#if JSRH_V8_AT_LEAST(11, 9) std::unique_ptr CreateBlockingScope(v8::BlockingType blockingType) override { return m_inner->CreateBlockingScope(blockingType); } +#endif double MonotonicallyIncreasingTime() override { return m_inner->MonotonicallyIncreasingTime(); } +#if JSRH_V8_AT_LEAST(11, 9) int64_t CurrentClockTimeMilliseconds() override { return m_inner->CurrentClockTimeMilliseconds(); } +#endif double CurrentClockTimeMillis() override { return m_inner->CurrentClockTimeMillis(); } +#if JSRH_V8_AT_LEAST(11, 9) double CurrentClockTimeMillisecondsHighResolution() override { return m_inner->CurrentClockTimeMillisecondsHighResolution(); } +#endif StackTracePrinter GetStackTracePrinter() override { return m_inner->GetStackTracePrinter(); } v8::TracingController* GetTracingController() override { return m_inner->GetTracingController(); } void DumpWithoutCrashing() override { m_inner->DumpWithoutCrashing(); } v8::HighAllocationThroughputObserver* GetHighAllocationThroughputObserver() override { return m_inner->GetHighAllocationThroughputObserver(); } private: + std::shared_ptr WrapForegroundTaskRunner(v8::Isolate* isolate); + std::unique_ptr m_inner; std::mutex m_mutex; std::map> m_wakes; @@ -159,12 +184,12 @@ namespace Babylon v8::Isolate* m_isolate; }; - std::shared_ptr DispatchingPlatform::GetForegroundTaskRunner(v8::Isolate* isolate, v8::TaskPriority) + std::shared_ptr DispatchingPlatform::WrapForegroundTaskRunner(v8::Isolate* isolate) { - // The priority-aware overload is not implemented by libplatform's DefaultPlatform in - // this V8 version - the base class default returns nullptr - so always ask the inner - // platform for the plain per-isolate runner. The wrapper is cached because V8 calls - // this often and hands the result around by pointer. + // Always ask the inner platform for the plain per-isolate runner: libplatform's + // DefaultPlatform does not implement the priority-aware overload in either V8 version + // we build against. The wrapper is cached because V8 calls this often and hands the + // result around by pointer. std::scoped_lock lock{m_mutex}; auto& runner = m_taskRunners[isolate]; if (!runner) diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 0af5caa8..e25e6df6 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(UnitTestsJNI SHARED ${UNIT_TESTS_DIR}/Shared/Shared.cpp) target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") +target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_ENGINE="${NAPI_JAVASCRIPT_ENGINE}") target_compile_definitions(UnitTestsJNI PRIVATE ARCANA_TEST_HOOKS) target_include_directories(UnitTestsJNI diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 2dbc7619..9111db81 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -45,6 +45,7 @@ endif() add_executable(UnitTests ${SOURCES} ${SCRIPTS} ${TYPE_SCRIPTS} ${ASSETS}) target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") +target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_ENGINE="${NAPI_JAVASCRIPT_ENGINE}") # The V8JSI Node-API shim does not implement napi_create_dataview, so the # CreateDataViewRejectsOverflowingRange test is compiled out on that backend. diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 4255fd5f..562f2bff 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -6,6 +6,7 @@ Mocha.setup('bdd'); Mocha.reporter('spec'); declare const hostPlatform: string; +declare const hostEngine: string; declare const setExitCode: (code: number) => void; @@ -2151,29 +2152,29 @@ describe("FileReader", function () { describe("WebAssembly", function () { this.timeout(30000); + // Only the V8 AppRuntime pumps V8's foreground task queue, which is what lets these promises + // settle. The other engines' runtimes have the same class of gap and hang here instead of + // failing, so scope the suite rather than leave a 30s timeout on every non-V8 leg. + beforeEach(function () { + if (hostEngine !== "V8" || typeof WebAssembly === "undefined") { + this.skip(); + } + }); + // Minimal valid module: the 8-byte header (magic + version) and no sections. const emptyModule = new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]); it("should settle the promise returned by WebAssembly.compile", async function () { - if (typeof WebAssembly === "undefined") { - this.skip(); - } const module = await WebAssembly.compile(emptyModule); expect(module).to.be.an.instanceof(WebAssembly.Module); }); it("should settle the promise returned by WebAssembly.instantiate", async function () { - if (typeof WebAssembly === "undefined") { - this.skip(); - } const result = await WebAssembly.instantiate(emptyModule); expect(result.instance).to.be.an.instanceof(WebAssembly.Instance); }); it("should reject the promise returned by WebAssembly.compile for invalid bytes", async function () { - if (typeof WebAssembly === "undefined") { - this.skip(); - } let threw = false; try { await WebAssembly.compile(new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0xFF])); diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index d1c2aa44..07db1559 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -100,6 +100,7 @@ TEST(JavaScript, All) env.Global().Set("setExitCode", setExitCodeCallback); env.Global().Set("hostPlatform", Napi::Value::From(env, JSRUNTIMEHOST_PLATFORM)); + env.Global().Set("hostEngine", Napi::Value::From(env, JSRUNTIMEHOST_ENGINE)); }); Babylon::ScriptLoader loader{runtime};