// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #pragma once // Common JSG testing infrastructure #include #include #include #include #include namespace workerd::jsg::test { // Checks the evaluation of a blob of JS code under the given context and isolate types. template class Evaluator { // TODO(cleanup): `ConfigurationType` currently can optionally be specified to fix the build // in cases that the isolate includes types that require configuration, but currently the // type is always default-constructed. What if you want to specify a test config? public: explicit Evaluator(V8System& v8System, ConfigurationType config = {}) : v8System(v8System), config(config) {} IsolateType& getIsolate() { // Slightly more efficient to only instantiate each isolate type once (17s vs. 20s): static IsolateType isolate( v8System, v8::IsolateGroup::GetDefault(), config, kj::heap()); return isolate; } void expectEvalModule( kj::StringPtr code, kj::StringPtr expectedType, kj::StringPtr expectedValue) { getIsolate().runInLockScope([&](IsolateType::Lock& lock) { JSG_WITHIN_CONTEXT_SCOPE(lock, lock.template newContext().getHandle(lock.v8Isolate), [&](jsg::Lock& js) { // Compile code as "main" module CompilationObserver observer; auto modules = ModuleRegistryImpl::from(js); auto p = kj::Path::parse("main"); modules->add(p, jsg::ModuleRegistry::ModuleInfo(lock, "main", code, nullptr /* compile cache */, ModuleInfoCompileOption::BUNDLE, observer)); // Instantiate the module auto& moduleInfo = KJ_REQUIRE_NONNULL(modules->resolve(js, p)); auto module = moduleInfo.module.getHandle(js); jsg::instantiateModule(js, module); // Module has to export "run" function auto moduleNs = check(module->GetModuleNamespace()->ToObject(js.v8Context())); auto runValue = check(moduleNs->Get(js.v8Context(), v8StrIntern(lock.v8Isolate, "run"_kj))); v8::TryCatch catcher(js.v8Isolate); // Run the function to get the result. v8::Local result; if (v8::Function::Cast(*runValue) ->Call(js.v8Context(), js.v8Context()->Global(), 0, nullptr) .ToLocal(&result)) { v8::String::Utf8Value type(js.v8Isolate, result->TypeOf(js.v8Isolate)); v8::String::Utf8Value value(js.v8Isolate, result); KJ_EXPECT(*type == expectedType, *type, expectedType); KJ_EXPECT(*value == expectedValue, *value, expectedValue); } else if (catcher.HasCaught()) { v8::String::Utf8Value message(js.v8Isolate, catcher.Exception()); KJ_EXPECT(expectedType == "throws", expectedType, catcher.Exception()); KJ_EXPECT(*message == expectedValue, *message, expectedValue); } else { KJ_FAIL_EXPECT("returned empty handle but didn't throw exception?"); } }); }); } void expectEval(kj::StringPtr code, kj::StringPtr expectedType, kj::StringPtr expectedValue) { getIsolate().runInLockScope([&](IsolateType::Lock& lock) { JSG_WITHIN_CONTEXT_SCOPE(lock, lock.template newContext().getHandle(lock.v8Isolate), [&](jsg::Lock& js) { // Create a string containing the JavaScript source code. v8::Local source = jsg::v8Str(js.v8Isolate, code); // Compile the source code. v8::Local script; if (!v8::Script::Compile(js.v8Context(), source).ToLocal(&script)) { KJ_FAIL_EXPECT("code didn't parse", code); return; } v8::TryCatch catcher(js.v8Isolate); // Run the script to get the result. v8::Local result; if (script->Run(js.v8Context()).ToLocal(&result)) { v8::String::Utf8Value type(js.v8Isolate, result->TypeOf(js.v8Isolate)); v8::String::Utf8Value value(js.v8Isolate, result); KJ_EXPECT(*type == expectedType, *type, expectedType); KJ_EXPECT(*value == expectedValue, *value, expectedValue); } else if (catcher.HasCaught()) { v8::String::Utf8Value message(js.v8Isolate, catcher.Exception()); KJ_EXPECT(expectedType == "throws", expectedType, catcher.Exception()); KJ_EXPECT(*message == expectedValue, *message, expectedValue); } else { KJ_FAIL_EXPECT("returned empty handle but didn't throw exception?"); } }); }); } void setAllowEval(bool b) { getIsolate().runInLockScope([&](IsolateType::Lock& lock) { lock.setAllowEval(b); }); } void setCaptureThrowsAsRejections(bool b) { getIsolate().runInLockScope( [&](IsolateType::Lock& lock) { lock.setCaptureThrowsAsRejections(b); }); } void runMicrotasks() { getIsolate().runInLockScope([&](IsolateType::Lock& lock) { lock.runMicrotasks(); }); } void runMicrotasks(IsolateType::Lock& lock) { lock.runMicrotasks(); } // Run some C++ code in a new lock and context. template void run(Func&& func) { getIsolate().runInLockScope([&](IsolateType::Lock& lock) { JSG_WITHIN_CONTEXT_SCOPE(lock, lock.template newContext().getHandle(lock.v8Isolate), [&](jsg::Lock& js) { v8::TryCatch tryCatch(js.v8Isolate); try { func(lock); } catch (JsExceptionThrown&) { if (tryCatch.HasTerminated()) { KJ_FAIL_ASSERT("TerminateExecution() was called"); } else { KJ_ASSERT(tryCatch.HasCaught()); jsg::throwTunneledException(js.v8Isolate, tryCatch.Exception()); } } }); }); } private: V8System& v8System; ConfigurationType config; }; struct NumberBox: public Object { double value; explicit NumberBox(double value): value(value) {} NumberBox() = default; static Ref constructor(jsg::Lock& js, double value) { return js.alloc(value); } void increment() { value += 1; } void incrementBy(double amount) { value += amount; } void incrementByBox(NumberBox& amount) { value += amount.value; } double add(double other) { return value + other; } double addBox(NumberBox& other) { return value + other.value; } Ref addReturnBox(jsg::Lock& js, double other) { return js.alloc(value + other); } double addMultiple(NumberBox& a, double b, NumberBox& c) { return value + a.value + b + c.value; } double getValue() { return value; } void setValue(double newValue) { value = newValue; } Ref getBoxed(jsg::Lock& js) { return js.alloc(value); } void setBoxed(NumberBox& newValue) { value = newValue.value; } v8::Local getBoxedFromTypeHandler( jsg::Lock& js, const TypeHandler>& numberBoxTypeHandler) { return numberBoxTypeHandler.wrap(js, js.alloc(value)); } JSG_RESOURCE_TYPE(NumberBox) { JSG_METHOD(increment); JSG_METHOD(incrementBy); JSG_METHOD(incrementByBox); JSG_METHOD(add); JSG_METHOD(addBox); JSG_METHOD(addReturnBox); JSG_METHOD(addMultiple); JSG_METHOD(getValue); JSG_METHOD(setValue); JSG_INSTANCE_PROPERTY(value, getValue, setValue); JSG_INSTANCE_PROPERTY(boxed, getBoxed, setBoxed); JSG_READONLY_INSTANCE_PROPERTY(boxedFromTypeHandler, getBoxedFromTypeHandler); } }; class BoxBox: public Object { public: explicit BoxBox(Ref inner): inner(kj::mv(inner)) {} Ref inner; static Ref constructor(jsg::Lock& js, NumberBox& inner, double add) { return js.alloc(js.alloc(inner.value + add)); } Ref getInner() { return inner.addRef(); } JSG_RESOURCE_TYPE(BoxBox) { JSG_READONLY_INSTANCE_PROPERTY(inner, getInner); } private: void visitForGc(GcVisitor& visitor) { visitor.visit(inner); } }; struct ExtendedNumberBox: public NumberBox { static Ref constructor(jsg::Lock& js, double value, kj::String text) { auto result = js.alloc(); result->value = value; result->text = kj::mv(text); return result; } kj::StringPtr getText() { return text; } void setText(kj::String newText) { text = kj::mv(newText); } kj::String text; JSG_RESOURCE_TYPE(ExtendedNumberBox) { JSG_INHERIT(NumberBox); JSG_METHOD(getText); JSG_METHOD(setText); JSG_INSTANCE_PROPERTY(text, getText, setText); } }; struct TestStruct { kj::String str; double num; Ref box; JSG_STRUCT(str, num, box); void visitForGc(GcVisitor& visitor) { visitor.visit(box); } }; } // namespace workerd::jsg::test