mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Allow to check whether modules finished evaluating
This commit is contained in:
parent
32ef4fd0fa
commit
ce01bdddf6
6 changed files with 202 additions and 16 deletions
|
|
@ -0,0 +1,3 @@
|
|||
await undefined;
|
||||
// `import "blabbermouth.js";` would be hoisted before the await resulting in it not being delayed.
|
||||
log("blah blah blah");
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
await undefined;
|
||||
await undefined;
|
||||
|
|
@ -0,0 +1 @@
|
|||
await new Promise(() => {});
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
#include "ps/CStr.h"
|
||||
#include "ps/Filesystem.h"
|
||||
#include "scriptinterface/Object.h"
|
||||
#include "scriptinterface/Promises.h"
|
||||
#include "scriptinterface/ScriptConversions.h"
|
||||
#include "scriptinterface/ScriptInterface.h"
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ namespace
|
|||
return std::get<1>(*std::get<0>(insertResult)).m_ModuleObject;
|
||||
}
|
||||
|
||||
void Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
|
||||
[[nodiscard]] JSObject* Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
|
||||
{
|
||||
if (!JS::ModuleLink(rq.cx, mod))
|
||||
{
|
||||
|
|
@ -72,12 +73,33 @@ void Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
|
|||
}
|
||||
|
||||
JS::RootedValue val{rq.cx};
|
||||
if (!JS::ModuleEvaluate(rq.cx, mod, &val))
|
||||
if (!JS::ModuleEvaluate(rq.cx, mod, &val) || !val.isObject())
|
||||
{
|
||||
ScriptException::CatchPending(rq);
|
||||
throw std::invalid_argument{"Unable to evaluate module."};
|
||||
}
|
||||
|
||||
return &val.toObject();
|
||||
}
|
||||
|
||||
bool Call(JSContext* cx, const unsigned argc, JS::Value* vp)
|
||||
{
|
||||
JS::CallArgs args{JS::CallArgsFromVp(argc, vp)};
|
||||
const ScriptRequest rq{cx};
|
||||
|
||||
const auto statusPtr{JS::GetMaybePtrFromReservedSlot<ModuleLoader::Future::Status>(
|
||||
&args.callee(), 0)};
|
||||
if (!statusPtr)
|
||||
return true;
|
||||
|
||||
(*statusPtr) = ModuleLoader::Future::Fulfilled{};
|
||||
return true;
|
||||
}
|
||||
|
||||
constexpr JSClassOps callbackClassOps{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
/*call =*/Call, nullptr, nullptr};
|
||||
|
||||
constexpr JSClass callbackClass{"Callback", JSCLASS_HAS_RESERVED_SLOTS(1), &callbackClassOps};
|
||||
} // anonymous namespace
|
||||
|
||||
ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsPath& filePath):
|
||||
|
|
@ -103,10 +125,56 @@ ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsP
|
|||
}
|
||||
}
|
||||
|
||||
void ModuleLoader::LoadModule(const ScriptRequest& rq, const VfsPath& modulePath)
|
||||
ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath):
|
||||
m_Status{Evaluating{{rq.cx, JS_NewObject(rq.cx, &callbackClass)}}}
|
||||
{
|
||||
JS::RootedObject mod{rq.cx, CompileModule(rq, m_Registry, modulePath)};
|
||||
Evaluate(rq, mod);
|
||||
JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_Registry, modulePath)};
|
||||
JS::RootedObject promise{rq.cx, Evaluate(rq, mod)};
|
||||
|
||||
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
|
||||
|
||||
if (!JS::AddPromiseReactions(rq.cx, promise, std::get<Evaluating>(m_Status).fulfill, nullptr))
|
||||
throw std::runtime_error{"Failed adding promise reaction."};
|
||||
}
|
||||
|
||||
ModuleLoader::Future::Future(Future&& other) noexcept:
|
||||
m_Status{std::exchange(other.m_Status, Invalid{})}
|
||||
{
|
||||
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
|
||||
}
|
||||
|
||||
ModuleLoader::Future& ModuleLoader::Future::operator=(Future&& other) noexcept
|
||||
{
|
||||
SetReservedSlot(JS::UndefinedValue());
|
||||
m_Status = std::exchange(other.m_Status, Invalid{});
|
||||
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
ModuleLoader::Future::~Future()
|
||||
{
|
||||
SetReservedSlot(JS::UndefinedValue());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ModuleLoader::Future::IsDone() const noexcept
|
||||
{
|
||||
return std::holds_alternative<Fulfilled>(m_Status);
|
||||
}
|
||||
|
||||
void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
|
||||
{
|
||||
Evaluating* evaluatingStatus{std::get_if<Evaluating>(&m_Status)};
|
||||
if (!evaluatingStatus)
|
||||
return;
|
||||
if (evaluatingStatus->fulfill)
|
||||
JS::SetReservedSlot(evaluatingStatus->fulfill, 0, privateValue);
|
||||
}
|
||||
|
||||
[[nodiscard]] ModuleLoader::Future ModuleLoader::LoadModule(const ScriptRequest& rq,
|
||||
const VfsPath& modulePath)
|
||||
{
|
||||
return Future{rq, *this, modulePath};
|
||||
}
|
||||
|
||||
[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
#include "scriptinterface/ScriptTypes.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
|
||||
class ScriptContext;
|
||||
class ScriptRequest;
|
||||
|
|
@ -42,13 +43,43 @@ public:
|
|||
|
||||
using RegistryType = std::unordered_map<VfsPath, CompiledModule>;
|
||||
|
||||
class Future
|
||||
{
|
||||
public:
|
||||
struct Evaluating
|
||||
{
|
||||
JS::PersistentRootedObject fulfill;
|
||||
};
|
||||
struct Fulfilled {};
|
||||
struct Invalid {};
|
||||
using Status = std::variant<Evaluating, Fulfilled, Invalid>;
|
||||
|
||||
explicit Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath);
|
||||
Future() = default;
|
||||
Future(const Future&) = delete;
|
||||
Future& operator=(const Future&) = delete;
|
||||
Future(Future&& other) noexcept;
|
||||
Future& operator=(Future&& other) noexcept;
|
||||
~Future();
|
||||
|
||||
[[nodiscard]] bool IsDone() const noexcept;
|
||||
|
||||
private:
|
||||
// It's save to not require a `JS::HandleValue` here.
|
||||
void SetReservedSlot(JS::Value privateValue) noexcept;
|
||||
|
||||
Status m_Status{Invalid{}};
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the specified module and all module it imports recursively.
|
||||
*
|
||||
* @param rq @c globalThis is taken from this @c ScriptRequest.
|
||||
* @param modulePath The path to the file which should be loaded as a module.
|
||||
* @return A future that is fulfilled when the evaluation of the module
|
||||
* completes.
|
||||
*/
|
||||
void LoadModule(const ScriptRequest& rq, const VfsPath& modulePath);
|
||||
[[nodiscard]] Future LoadModule(const ScriptRequest& rq, const VfsPath& modulePath);
|
||||
|
||||
private:
|
||||
// Functions used by the `ScriptContext`.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include "ps/CLogger.h"
|
||||
#include "ps/Filesystem.h"
|
||||
#include "scriptinterface/ModuleLoader.h"
|
||||
#include "scriptinterface/ScriptContext.h"
|
||||
#include "scriptinterface/ScriptInterface.h"
|
||||
|
||||
class TestScriptModule : public CxxTest::TestSuite
|
||||
|
|
@ -43,7 +44,7 @@ public:
|
|||
const ScriptRequest rq{script};
|
||||
|
||||
TestLogger logger;
|
||||
script.GetModuleLoader().LoadModule(rq, "include/entry.js");
|
||||
std::ignore = script.GetModuleLoader().LoadModule(rq, "include/entry.js");
|
||||
|
||||
// This test does not rely on export to the engine. So use the logger to check if it succeeded.
|
||||
TS_ASSERT_STR_CONTAINS(logger.GetOutput(),"Test succeeded");
|
||||
|
|
@ -54,12 +55,12 @@ public:
|
|||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
script.GetModuleLoader().LoadModule(rq, "empty.js");
|
||||
std::ignore = script.GetModuleLoader().LoadModule(rq, "empty.js");
|
||||
}
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
script.GetModuleLoader().LoadModule(rq, "empty.js");
|
||||
std::ignore = script.GetModuleLoader().LoadModule(rq, "empty.js");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,9 +71,9 @@ public:
|
|||
{
|
||||
ScriptInterface scriptInner{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rqInner{scriptInner};
|
||||
scriptInner.GetModuleLoader().LoadModule(rqInner, "empty.js");
|
||||
std::ignore = scriptInner.GetModuleLoader().LoadModule(rqInner, "empty.js");
|
||||
}
|
||||
scriptOuter.GetModuleLoader().LoadModule(rqOuter, "empty.js");
|
||||
std::ignore = scriptOuter.GetModuleLoader().LoadModule(rqOuter, "empty.js");
|
||||
}
|
||||
|
||||
void test_ImportInFunction()
|
||||
|
|
@ -81,8 +82,8 @@ public:
|
|||
const ScriptRequest rq{script};
|
||||
|
||||
TestLogger logger;
|
||||
TS_ASSERT_THROWS(script.GetModuleLoader().LoadModule(rq, "import_inside_function.js"),
|
||||
const std::invalid_argument&);
|
||||
TS_ASSERT_THROWS(std::ignore = script.GetModuleLoader().LoadModule(rq,
|
||||
"import_inside_function.js"), const std::invalid_argument&);
|
||||
const std::string log{logger.GetOutput()};
|
||||
TS_ASSERT_STR_CONTAINS(log, "import_inside_function.js line 3");
|
||||
TS_ASSERT_STR_CONTAINS(log, "import declarations may only appear at top level of a module");
|
||||
|
|
@ -94,7 +95,7 @@ public:
|
|||
const ScriptRequest rq{script};
|
||||
|
||||
const TestLogger _;
|
||||
TS_ASSERT_THROWS(script.GetModuleLoader().LoadModule(rq, "nonexistent.js"),
|
||||
TS_ASSERT_THROWS(std::ignore = script.GetModuleLoader().LoadModule(rq, "nonexistent.js"),
|
||||
const std::runtime_error&);
|
||||
}
|
||||
|
||||
|
|
@ -105,13 +106,93 @@ public:
|
|||
|
||||
{
|
||||
TestLogger logger;
|
||||
script.GetModuleLoader().LoadModule(rq, "blabbermouth.js");
|
||||
std::ignore = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js");
|
||||
TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
}
|
||||
{
|
||||
TestLogger logger;
|
||||
script.GetModuleLoader().LoadModule(rq, "include/../blabbermouth.js");
|
||||
std::ignore = script.GetModuleLoader().LoadModule(rq, "include/../blabbermouth.js");
|
||||
TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
}
|
||||
}
|
||||
|
||||
void test_TopLevelAwaitFinite()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
|
||||
|
||||
TS_ASSERT(!future.IsDone());
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future.IsDone());
|
||||
}
|
||||
|
||||
void test_TopLevelAwaitInfinite()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_infinite.js");
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(!future.IsDone());
|
||||
}
|
||||
|
||||
void test_MoveFulfilledFuture()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
Script::ModuleLoader::Future future0{
|
||||
script.GetModuleLoader().LoadModule(rq, "empty.js")};
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future0.IsDone());
|
||||
|
||||
Script::ModuleLoader::Future future1{std::move(future0)};
|
||||
Script::ModuleLoader::Future future2;
|
||||
future2 = std::move(future1);
|
||||
|
||||
TS_ASSERT(!future0.IsDone());
|
||||
TS_ASSERT(!future1.IsDone());
|
||||
TS_ASSERT(future2.IsDone());
|
||||
}
|
||||
|
||||
void test_MoveEvaluatingFuture()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
Script::ModuleLoader::Future future0{
|
||||
script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")};
|
||||
Script::ModuleLoader::Future future1{std::move(future0)};
|
||||
Script::ModuleLoader::Future future2;
|
||||
future2 = std::move(future1);
|
||||
|
||||
TS_ASSERT(!future0.IsDone());
|
||||
TS_ASSERT(!future1.IsDone());
|
||||
TS_ASSERT(!future2.IsDone());
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(!future0.IsDone());
|
||||
TS_ASSERT(!future1.IsDone());
|
||||
TS_ASSERT(future2.IsDone());
|
||||
}
|
||||
|
||||
void test_EvaluateReplacedFuture()
|
||||
{
|
||||
ScriptInterface script{"Test", "Test", g_ScriptContext};
|
||||
const ScriptRequest rq{script};
|
||||
|
||||
TestLogger logger;
|
||||
auto future{script.GetModuleLoader().LoadModule(rq, "delayed_blabbermouth.js")};
|
||||
TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
TS_ASSERT(!future.IsDone());
|
||||
|
||||
future = script.GetModuleLoader().LoadModule(rq, "empty.js");
|
||||
TS_ASSERT(!future.IsDone());
|
||||
|
||||
g_ScriptContext->RunJobs();
|
||||
TS_ASSERT(future.IsDone());
|
||||
TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue