diff --git a/Makefile b/Makefile index 68e222656eb980..35eeeb618b7097 100644 --- a/Makefile +++ b/Makefile @@ -212,6 +212,8 @@ coverage-clean: $(RM) out/$(BUILDTYPE)/obj.target/node/src/tracing/*.gcno $(RM) out/$(BUILDTYPE)/obj.target/cctest/src/*.gcno $(RM) out/$(BUILDTYPE)/obj.target/cctest/test/cctest/*.gcno + $(RM) out/$(BUILDTYPE)/obj.target/embedtest/src/*.gcno + $(RM) out/$(BUILDTYPE)/obj.target/embedtest/test/embedding/*.gcno .PHONY: coverage # Build and test with code coverage reporting. Leave the lib directory @@ -250,8 +252,8 @@ coverage-test: coverage-build TEST_CI_ARGS="$(TEST_CI_ARGS) --type=coverage" $(MAKE) $(COVTESTS) $(MAKE) coverage-report-js -(cd out && "../gcovr/scripts/gcovr" \ - --gcov-exclude='.*\b(deps|usr|out|cctest)\b' -v -r Release/obj.target \ - --html --html-detail -o ../coverage/cxxcoverage.html \ + --gcov-exclude='.*\b(deps|usr|out|cctest|embedding)\b' -v \ + -r Release/obj.target --html --html-detail -o ../coverage/cxxcoverage.html \ --gcov-executable="$(GCOV)") @echo -n "Javascript coverage %: " @grep -B1 Lines coverage/index.html | head -n1 \ @@ -276,6 +278,7 @@ coverage-report-js: # Runs the C++ tests using the built `cctest` executable. cctest: all @out/$(BUILDTYPE)/$@ --gtest_filter=$(GTEST_FILTER) + @out/$(BUILDTYPE)/embedtest "require('./test/embedding/test.js')" .PHONY: list-gtests list-gtests: @@ -531,6 +534,7 @@ test-ci: | clear-stalled bench-addons-build build-addons build-js-native-api-tes $(PYTHON) tools/test.py $(PARALLEL_ARGS) -p tap --logfile test.tap \ --mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \ $(TEST_CI_ARGS) $(CI_JS_SUITES) $(CI_NATIVE_SUITES) $(CI_DOC) + out/Release/embedtest 'require("./test/embedding/test.js")' @echo "Clean up any leftover processes, error if found." ps awwx | grep Release/node | grep -v grep | cat @PS_OUT=`ps awwx | grep Release/node | grep -v grep | awk '{print $$1}'`; \ @@ -1274,6 +1278,8 @@ LINT_CPP_FILES = $(filter-out $(LINT_CPP_EXCLUDE), $(wildcard \ test/addons/*/*.h \ test/cctest/*.cc \ test/cctest/*.h \ + test/embedding/*.cc \ + test/embedding/*.h \ test/js-native-api/*/*.cc \ test/js-native-api/*/*.h \ test/node-api/*/*.cc \ diff --git a/node.gyp b/node.gyp index 8173a1afdda8ee..c03623846a2b6e 100644 --- a/node.gyp +++ b/node.gyp @@ -1236,6 +1236,62 @@ ], }, # cctest + { + 'target_name': 'embedtest', + 'type': 'executable', + + 'dependencies': [ + '<(node_lib_target_name)', + 'deps/histogram/histogram.gyp:histogram', + 'deps/uvwasi/uvwasi.gyp:uvwasi', + 'node_dtrace_header', + 'node_dtrace_ustack', + 'node_dtrace_provider', + ], + + 'includes': [ + 'node.gypi' + ], + + 'include_dirs': [ + 'src', + 'tools/msvs/genfiles', + 'deps/v8/include', + 'deps/cares/include', + 'deps/uv/include', + 'deps/uvwasi/include', + 'test/embedding', + ], + + 'sources': [ + 'src/node_snapshot_stub.cc', + 'src/node_code_cache_stub.cc', + 'test/embedding/embedtest.cc', + ], + + 'conditions': [ + ['OS=="solaris"', { + 'ldflags': [ '-I<(SHARED_INTERMEDIATE_DIR)' ] + }], + # Skip cctest while building shared lib node for Windows + [ 'OS=="win" and node_shared=="true"', { + 'type': 'none', + }], + [ 'node_shared=="true"', { + 'xcode_settings': { + 'OTHER_LDFLAGS': [ '-Wl,-rpath,@loader_path', ], + }, + }], + ['OS=="win"', { + 'libraries': [ + 'Dbghelp.lib', + 'winmm.lib', + 'Ws2_32.lib', + ], + }], + ], + }, # embedtest + # TODO(joyeecheung): do not depend on node_lib, # instead create a smaller static library node_lib_base that does # just enough for node_native_module.cc and the cache builder to diff --git a/src/node_code_cache_stub.cc b/src/node_code_cache_stub.cc index 3851508170f812..9d9901738f63b0 100644 --- a/src/node_code_cache_stub.cc +++ b/src/node_code_cache_stub.cc @@ -1,3 +1,7 @@ +// This file is part of the embedder test, which is intentionally built without +// NODE_WANT_INTERNALS, so we define it here manually. +#define NODE_WANT_INTERNALS 1 + #include "node_native_module_env.h" // The stub here is used when configure is run without `--code-cache-path`. diff --git a/src/node_snapshot_stub.cc b/src/node_snapshot_stub.cc index 91bc37121d61fe..fac03b0c87af5d 100644 --- a/src/node_snapshot_stub.cc +++ b/src/node_snapshot_stub.cc @@ -1,3 +1,7 @@ +// This file is part of the embedder test, which is intentionally built without +// NODE_WANT_INTERNALS, so we define it here manually. +#define NODE_WANT_INTERNALS 1 + #include "node_main_instance.h" namespace node { diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc new file mode 100644 index 00000000000000..366c8f12e2c11f --- /dev/null +++ b/test/embedding/embedtest.cc @@ -0,0 +1,135 @@ +#include "node.h" +#include "uv.h" +#include + +// Note: This file is being referred to from doc/api/embedding.md, and excerpts +// from it are included in the documentation. Try to keep these in sync. + +using node::ArrayBufferAllocator; +using node::Environment; +using node::IsolateData; +using node::MultiIsolatePlatform; +using v8::Context; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Locker; +using v8::MaybeLocal; +using v8::SealHandleScope; +using v8::Value; +using v8::V8; + +static int RunNodeInstance(MultiIsolatePlatform* platform, + const std::vector& args, + const std::vector& exec_args); + +int main(int argc, char** argv) { + std::vector args(argv, argv + argc); + std::vector exec_args; + std::vector errors; + int exit_code = node::InitializeNodeWithArgs(&args, &exec_args, &errors); + for (const std::string& error : errors) + fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str()); + if (exit_code != 0) { + return exit_code; + } + + std::unique_ptr platform = + MultiIsolatePlatform::Create(4); + V8::InitializePlatform(platform.get()); + V8::Initialize(); + + int ret = RunNodeInstance(platform.get(), args, exec_args); + + V8::Dispose(); + V8::ShutdownPlatform(); + return ret; +} + +int RunNodeInstance(MultiIsolatePlatform* platform, + const std::vector& args, + const std::vector& exec_args) { + int exit_code = 0; + uv_loop_t loop; + int ret = uv_loop_init(&loop); + if (ret != 0) { + fprintf(stderr, "%s: Failed to initialize loop: %s\n", + args[0].c_str(), + uv_err_name(ret)); + return 1; + } + + std::shared_ptr allocator = + ArrayBufferAllocator::Create(); + + Isolate* isolate = NewIsolate(allocator.get(), &loop, platform); + if (isolate == nullptr) { + fprintf(stderr, "%s: Failed to initialize V8 Isolate\n", args[0].c_str()); + return 1; + } + + { + Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + + std::unique_ptr isolate_data( + node::CreateIsolateData(isolate, &loop, platform, allocator.get()), + node::FreeIsolateData); + + HandleScope handle_scope(isolate); + Local context = node::NewContext(isolate); + if (context.IsEmpty()) { + fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str()); + return 1; + } + + Context::Scope context_scope(context); + std::unique_ptr env( + node::CreateEnvironment(isolate_data.get(), context, args, exec_args), + node::FreeEnvironment); + + MaybeLocal loadenv_ret = node::LoadEnvironment( + env.get(), + "const publicRequire =" + " require('module').createRequire(process.cwd() + '/');" + "globalThis.require = publicRequire;" + "require('vm').runInThisContext(process.argv[1]);"); + + if (loadenv_ret.IsEmpty()) // There has been a JS exception. + return 1; + + { + SealHandleScope seal(isolate); + bool more; + do { + uv_run(&loop, UV_RUN_DEFAULT); + + platform->DrainTasks(isolate); + more = uv_loop_alive(&loop); + if (more) continue; + + node::EmitBeforeExit(env.get()); + more = uv_loop_alive(&loop); + } while (more == true); + } + + exit_code = node::EmitExit(env.get()); + + node::Stop(env.get()); + } + + bool platform_finished = false; + platform->AddIsolateFinishedCallback(isolate, [](void* data) { + *static_cast(data) = true; + }, &platform_finished); + platform->UnregisterIsolate(isolate); + isolate->Dispose(); + + // Wait until the platform has cleaned up all relevant resources. + while (!platform_finished) + uv_run(&loop, UV_RUN_ONCE); + int err = uv_loop_close(&loop); + assert(err == 0); + + return exit_code; +} diff --git a/test/embedding/test.js b/test/embedding/test.js new file mode 100644 index 00000000000000..a802de1849021f --- /dev/null +++ b/test/embedding/test.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const child_process = require('child_process'); + +common.allowGlobals(global.require); + +assert.strictEqual( + child_process.spawnSync(process.execPath, ['console.log(42)']) + .stdout.toString().trim(), + '42'); + +assert.strictEqual( + child_process.spawnSync(process.execPath, ['throw new Error()']).status, + 1); + +assert.strictEqual( + child_process.spawnSync(process.execPath, ['process.exitCode = 8']).status, + 8);