current commit
261007.41 · src, test: migrate to cppgc-owned v8::MicrotaskQueue
#include "libplatform/libplatform.h"
#include "node_buffer.h"
#include "node_internals.h"
#include "node_realm-inl.h"
#include "node_snapshot_builder.h"
#include "node_url.h"
#include "util.h"

#include <stdio.h>
#include <cstdio>
#include <string>
#include <thread>  // NOLINT(build/c++11)
#include "gtest/gtest.h"
#include "node_test_fixture.h"

using node::AtExit;
using node::RunAtExit;
using node::USE;
using v8::Context;
using v8::Local;

static bool called_cb_1 = false;
static bool called_cb_2 = false;
static bool called_cb_ordered_1 = false;
static bool called_cb_ordered_2 = false;
static bool called_at_exit_js = false;
static void at_exit_callback1(void* arg);
static void at_exit_callback2(void* arg);
static void at_exit_callback_ordered1(void* arg);
static void at_exit_callback_ordered2(void* arg);
static void at_exit_js(void* arg);
static std::string cb_1_arg;  // NOLINT(runtime/string)

struct SelfRemovingCleanupHookState {
  v8::Isolate* isolate;
  bool ran = false;
};
static void self_removing_cleanup_hook(void* arg);

class EnvironmentTest : public EnvironmentTestFixture {
 private:
  void TearDown() override {
    EnvironmentTestFixture::TearDown();
    called_cb_1 = false;
    called_cb_2 = false;
    called_cb_ordered_1 = false;
    called_cb_ordered_2 = false;
  }
};

TEST_F(EnvironmentTest, ManagedBufferCache) {
  constexpr size_t kCacheSize = 64 * 1024;
  constexpr size_t kOtherSize = 1024;
  const v8::HandleScope handle_scope(isolate_);
  Argv argv;
  Env env{handle_scope, argv};

  (*env)->recycle_managed_buffer(nullptr);

  uv_buf_t buffer = (*env)->allocate_managed_buffer(kCacheSize);
  char* cached_data = buffer.base;
  (*env)->recycle_managed_buffer((*env)->release_managed_buffer(buffer));

  buffer = (*env)->allocate_managed_buffer(kOtherSize);
  (*env)->recycle_managed_buffer((*env)->release_managed_buffer(buffer));

  buffer = (*env)->allocate_managed_buffer(kCacheSize);
  EXPECT_EQ(buffer.base, cached_data);
  (*env)->release_managed_buffer(buffer);
}

static int embedder_wasm_streaming_calls = 0;
static void EmbedderWasmStreaming(const v8::FunctionCallbackInfo<v8::Value>&) {
  embedder_wasm_streaming_calls++;
}

TEST_F(EnvironmentTest, KeepsEmbedderWasmStreamingCallbackWhenAsked) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};

  auto compile_streaming = [&]() {
    v8::Local<v8::Context> context = isolate_->GetCurrentContext();
    v8::Script::Compile(context,
                        v8::String::NewFromUtf8Literal(
                            isolate_, "WebAssembly.compileStreaming(0); 0"))
        .ToLocalChecked()
        ->Run(context)
        .ToLocalChecked();
    isolate_->PerformMicrotaskCheckpoint();
  };

  node::IsolateSettings settings;
  settings.flags |= node::SHOULD_NOT_SET_WASM_STREAMING_CALLBACK;
  isolate_->SetWasmStreamingCallback(EmbedderWasmStreaming);
  node::SetIsolateUpForNode(isolate_, settings);
  compile_streaming();
  EXPECT_EQ(embedder_wasm_streaming_calls, 1);

  node::SetIsolateUpForNode(isolate_);
  compile_streaming();
  EXPECT_EQ(embedder_wasm_streaming_calls, 1);
}

TEST_F(EnvironmentTest, EnvironmentWithoutBrowserGlobals) {
  const v8::HandleScope handle_scope(isolate_);
  Argv argv;
  Env env{handle_scope, argv, node::EnvironmentFlags::kNoBrowserGlobals};

  SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) {
    EXPECT_EQ(*env, env_);
    EXPECT_EQ(exit_code, 0);
    node::Stop(*env);
  });

  node::LoadEnvironment(
      *env,
      "const assert = require('assert');"
      "const path = require('path');"
      "const relativeRequire = "
      "  require('module').createRequire(path.join(process.cwd(), 'stub.js'));"
      "const { intrinsics, nodeGlobals } = "
      "  relativeRequire('./test/common/globals');"
      "const items = Object.getOwnPropertyNames(globalThis);"
      "const leaks = [];"
      "for (const item of items) {"
      "  if (intrinsics.has(item)) {"
      "    continue;"
      "  }"
      "  if (nodeGlobals.has(item)) {"
      "    continue;"
      "  }"
      "  leaks.push(item);"
      "}"
      "assert.deepStrictEqual(leaks, []);");
}

TEST_F(EnvironmentTest, EnvironmentWithESMLoader) {
  const v8::HandleScope handle_scope(isolate_);
  Argv argv;
  Env env {handle_scope, argv};

  node::Environment* envi = *env;
  envi->options()->experimental_vm_modules = true;

  SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) {
    EXPECT_EQ(*env, env_);
    EXPECT_EQ(exit_code, 0);
    node::Stop(*env);
  });

  node::LoadEnvironment(
      *env,
      "const { SourceTextModule } = require('vm');"
      "(async () => {"
        "const stmString = 'globalThis.importResult = import(\"\")';"
        "const m = new SourceTextModule(stmString, {"
          "importModuleDynamically: (async () => {"
            "const m = new SourceTextModule('');"
            "await m.link(() => 0);"
            "await m.evaluate();"
            "return m.namespace;"
          "}),"
        "});"
        "await m.link(() => 0);"
        "await m.evaluate();"
        "delete globalThis.importResult;"
        "process.exit(0);"
      "})()");
}

class RedirectStdErr {
 public:
  explicit RedirectStdErr(const char* filename) : filename_(filename) {
    fflush(stderr);
    fgetpos(stderr, &pos_);
    fd_ = dup(fileno(stderr));
    USE(freopen(filename_, "w", stderr));
  }

  ~RedirectStdErr() {
    fflush(stderr);
    dup2(fd_, fileno(stderr));
    close(fd_);
    remove(filename_);
    clearerr(stderr);
    fsetpos(stderr, &pos_);
  }

 private:
  int fd_;
  fpos_t pos_;
  const char* filename_;
};

TEST_F(EnvironmentTest, EnvironmentWithNoESMLoader) {
  // The following line will cause stderr to get redirected to avoid the
  // error that would otherwise be printed to the console by this test.
  RedirectStdErr redirect_scope("environment_test.log");
  const v8::HandleScope handle_scope(isolate_);
  Argv argv;
  Env env {handle_scope, argv, node::EnvironmentFlags::kNoRegisterESMLoader};

  node::Environment* envi = *env;
  envi->options()->experimental_vm_modules = true;

  SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) {
    EXPECT_EQ(*env, env_);
    EXPECT_EQ(exit_code, 1);
    node::Stop(*env);
  });

  node::LoadEnvironment(
      *env,
      "const { SourceTextModule } = require('vm');"
      "(async () => {"
        "const stmString = 'globalThis.importResult = import(\"\")';"
        "const m = new SourceTextModule(stmString, {"
          "importModuleDynamically: (async () => {"
            "const m = new SourceTextModule('');"
            "await m.link(() => 0);"
            "await m.evaluate();"
            "return m.namespace;"
          "}),"
        "});"
        "await m.link(() => 0);"
        "await m.evaluate();"
        "delete globalThis.importResult;"
      "})()");
}

TEST_F(EnvironmentTest, PreExecutionPreparation) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  node::LoadEnvironment(*env, [&](const node::StartExecutionCallbackInfo& info)
                                  -> v8::MaybeLocal<v8::Value> {
    return v8::Null(isolate_);
  });

  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
  const char* run_script = "process.argv0";
  v8::Local<v8::Script> script = v8::Script::Compile(
      context,
      v8::String::NewFromOneByte(isolate_,
                                 reinterpret_cast<const uint8_t*>(run_script))
                                 .ToLocalChecked())
      .ToLocalChecked();
  v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
  CHECK(result->IsString());
}

TEST_F(EnvironmentTest, LoadEnvironmentWithCallback) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
  bool called_cb = false;
  node::LoadEnvironment(*env,
                        [&](const node::StartExecutionCallbackInfo& info)
                            -> v8::MaybeLocal<v8::Value> {
    called_cb = true;

    CHECK(info.process_object->IsObject());
    CHECK(info.native_require->IsFunction());

    v8::Local<v8::Value> argv0 = info.process_object->Get(
        context,
        v8::String::NewFromOneByte(
            isolate_,
            reinterpret_cast<const uint8_t*>("argv0"))
            .ToLocalChecked()).ToLocalChecked();
    CHECK(argv0->IsString());

    return info.process_object;
  });

  CHECK(called_cb);
}

TEST_F(EnvironmentTest, LoadEnvironmentWithSource) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
  v8::Local<v8::Value> main_ret =
      node::LoadEnvironment(*env,
                            "return { process, require };").ToLocalChecked();

  CHECK(main_ret->IsObject());
  CHECK(main_ret.As<v8::Object>()->Get(
      context,
      v8::String::NewFromOneByte(
          isolate_,
          reinterpret_cast<const uint8_t*>("process"))
          .ToLocalChecked())
          .ToLocalChecked()->IsObject());
  CHECK(main_ret.As<v8::Object>()->Get(
      context,
      v8::String::NewFromOneByte(
          isolate_,
          reinterpret_cast<const uint8_t*>("require"))
          .ToLocalChecked())
          .ToLocalChecked()->IsFunction());
}

TEST_F(EnvironmentTest, AtExitWithEnvironment) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  AtExit(*env, at_exit_callback1, nullptr);
  RunAtExit(*env);
  EXPECT_TRUE(called_cb_1);
}

TEST_F(EnvironmentTest, AtExitOrder) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  // Test that callbacks are run in reverse order.
  AtExit(*env, at_exit_callback_ordered1, nullptr);
  AtExit(*env, at_exit_callback_ordered2, nullptr);
  RunAtExit(*env);
  EXPECT_TRUE(called_cb_ordered_1);
  EXPECT_TRUE(called_cb_ordered_2);
}

TEST_F(EnvironmentTest, AtExitWithArgument) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  std::string arg{"some args"};
  AtExit(*env, at_exit_callback1, static_cast<void*>(&arg));
  RunAtExit(*env);
  EXPECT_EQ(arg, cb_1_arg);
}

TEST_F(EnvironmentTest, AtExitRunsJS) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  AtExit(*env, at_exit_js, static_cast<void*>(isolate_));
  EXPECT_FALSE(called_at_exit_js);
  RunAtExit(*env);
  EXPECT_TRUE(called_at_exit_js);
}

// A cleanup hook that removes itself while the environment cleanup queue is
// being drained must not cause a use-after-free. This registers such a hook
// directly rather than through node::ObjectWrap, whose destructor removes
// its own hook and is what makes this reachable for addons since #63642.
// The use-after-free is silent in ordinary builds; it is caught by the
// ASan/Valgrind CI, which is also how the original assertion (#63923)
// surfaced. Regression test for https://github.com/nodejs/node/issues/65195.
TEST_F(EnvironmentTest, RemoveEnvironmentCleanupHookDuringCleanup) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  SelfRemovingCleanupHookState state{isolate_};
  {
    Env env{handle_scope, argv};
    node::AddEnvironmentCleanupHook(
        isolate_, self_removing_cleanup_hook, &state);
    // Destroying `env` runs FreeEnvironment() -> RunCleanup(), which drains
    // the cleanup queue and invokes CleanupHookThunkRun() for the hook above.
  }
  EXPECT_TRUE(state.ran);
}

TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env1 {handle_scope, argv};
  Env env2{handle_scope, argv};

  AtExit(*env1, at_exit_callback1, nullptr);
  AtExit(*env2, at_exit_callback2, nullptr);
  RunAtExit(*env1);
  EXPECT_TRUE(called_cb_1);
  EXPECT_FALSE(called_cb_2);

  RunAtExit(*env2);
  EXPECT_TRUE(called_cb_2);
}

TEST_F(EnvironmentTest, WorkerInEnvironmentWithoutSnapshot) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};
  // When using certain experimental compile options, snapshot data may
  // not exist. Skip in that case.
  if (isolate_data_->snapshot_data()) {
    node::LoadEnvironment(*env,
                          "const { Worker } = require('worker_threads');"
                          "new Worker('process.exit(0)', { eval: true });")
        .ToLocalChecked();
    EXPECT_EQ(node::SpinEventLoop(*env).FromJust(), 0);
  }
}

TEST_F(EnvironmentTest, StopFromExitHandlerDoesNotLeakIntoNextEnvironment) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  {
    Env env{handle_scope, argv};
    node::SetProcessExitHandler(
        *env, [](node::Environment* env_, int) { node::Stop(env_); });
    // The uncaught exception runs the exit handler from C++ and does not
    // re-enter JS afterwards, so nothing consumes the termination request.
    EXPECT_TRUE(
        node::LoadEnvironment(*env, "throw new Error('uncaught')").IsEmpty());
    EXPECT_TRUE(node::SpinEventLoop(*env).IsNothing());
  }
  {
    Env env{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
    v8::Local<v8::Value> result =
        node::LoadEnvironment(*env, "return 42;").ToLocalChecked();
    EXPECT_EQ(result->Int32Value(env.context()).FromJust(), 42);
  }
}

TEST_F(EnvironmentTest, CollectExternalReferencesFromSeveralThreads) {
  constexpr int kThreads = 8;
  const intptr_t* data[kThreads];
  size_t sizes[kThreads];
  std::vector<std::thread> threads;
  for (int i = 0; i < kThreads; i++) {
    threads.emplace_back([&, i]() {
      const std::vector<intptr_t>& references =
          node::SnapshotBuilder::CollectExternalReferences();
      data[i] = references.data();
      sizes[i] = references.size();
    });
  }
  for (std::thread& thread : threads) thread.join();
  for (int i = 1; i < kThreads; i++) {
    EXPECT_EQ(data[i], data[0]);
    EXPECT_EQ(sizes[i], sizes[0]);
  }
  EXPECT_EQ(node::SnapshotBuilder::CollectExternalReferences().back(), 0);
}

TEST_F(EnvironmentTest, SharedIsolateDataLoadsBindingsTwice) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  const char* script =
      "for (const m of require('module').builtinModules) {"
      "  try { require(m); } catch {}"
      "}"
      "new (require('net').Socket)();"
#if HAVE_OPENSSL
      "require('tls').createSecureContext();"
      "require('crypto').createSecretKey(Buffer.alloc(8));"
#endif
      "new (require('worker_threads').MessageChannel)().port1.close();";
  Env env1{handle_scope, argv};
  node::LoadEnvironment(*env1, script).ToLocalChecked();
  EXPECT_EQ(node::SpinEventLoop(*env1).FromJust(), 0);
  Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
  node::LoadEnvironment(*env2, script).ToLocalChecked();
  EXPECT_EQ(node::SpinEventLoop(*env2).FromJust(), 0);
}

#if HAVE_INSPECTOR
TEST_F(EnvironmentTest, WorkerConnectToMainThreadWithoutInspector) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
  node::LoadEnvironment(
      *env,
      "const { Worker } = require('worker_threads');"
      "const w = new Worker(`"
      "  const { Session } = require('inspector');"
      "  try { new Session().connectToMainThread(); }"
      "  catch (e) { process.exit(e.code === 'ERR_INSPECTOR_NOT_AVAILABLE' ?"
      "    0 : 2); }"
      "  process.exit(3);"
      "`, { eval: true });"
      "w.on('exit', (code) => { process.exitCode = code; });")
      .ToLocalChecked();
  EXPECT_EQ(node::SpinEventLoop(*env).FromJust(), 0);
}
#endif  // HAVE_INSPECTOR

static int cleanup_hook_runs = 0;
static void CountingCleanupHook(void* arg) {
  cleanup_hook_runs++;
}

TEST_F(EnvironmentTest, SameCleanupHookInTwoEnvironmentsOnOneIsolate) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  cleanup_hook_runs = 0;
  {
    Env env1{handle_scope, argv};
    {
      Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
      {
        v8::Context::Scope context_scope(env1.context());
        node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, nullptr);
      }
      node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, nullptr);
    }
    EXPECT_EQ(cleanup_hook_runs, 1);
  }
  EXPECT_EQ(cleanup_hook_runs, 2);
}

TEST_F(EnvironmentTest, RemoveCleanupHookOfOtherEnvironmentOnSameIsolate) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  cleanup_hook_runs = 0;
  int arg;
  {
    Env env1{handle_scope, argv};
    node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, &arg);
    Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
    // env2's context is current; the hook belongs to env1.
    node::RemoveEnvironmentCleanupHook(isolate_, CountingCleanupHook, &arg);
  }
  EXPECT_EQ(cleanup_hook_runs, 0);
}

struct SelfRemovingHook {
  v8::Isolate* isolate;
  bool ran = false;
  static void Run(void* arg) {
    SelfRemovingHook* self = static_cast<SelfRemovingHook*>(arg);
    self->ran = true;
    node::RemoveEnvironmentCleanupHook(self->isolate, Run, arg);
  }
};

TEST_F(EnvironmentTest, CleanupHookRemovesItselfWhileRunning) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  SelfRemovingHook hook{isolate_};
  {
    Env env{handle_scope, argv};
    node::AddEnvironmentCleanupHook(isolate_, SelfRemovingHook::Run, &hook);
  }
  EXPECT_TRUE(hook.ran);
}

TEST_F(EnvironmentTest, FreeEnvironmentWhileSiblingHasActiveHandles) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env1{handle_scope, argv};
  node::LoadEnvironment(*env1,
                        "globalThis.ticks = 0;"
                        "const t = setInterval(() => {"
                        "  if (++globalThis.ticks == 20) clearInterval(t);"
                        "}, 1);")
      .ToLocalChecked();
  {
    Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
    node::LoadEnvironment(*env2, "setInterval(() => {}, 1);").ToLocalChecked();
    uv_sleep(5);
  }
  v8::Context::Scope context_scope(env1.context());
  EXPECT_EQ(node::SpinEventLoop(*env1).FromJust(), 0);
  v8::Local<v8::Value> ticks =
      env1.context()
          ->Global()
          ->Get(env1.context(),
                v8::String::NewFromUtf8Literal(isolate_, "ticks"))
          .ToLocalChecked();
  EXPECT_EQ(ticks->Int32Value(env1.context()).FromJust(), 20);
}

TEST_F(EnvironmentTest, NoEnvironmentSanity) {
  const v8::HandleScope handle_scope(isolate_);
  v8::Local<v8::Context> context = v8::Context::New(isolate_);
  EXPECT_EQ(node::Environment::GetCurrent(context), nullptr);
  EXPECT_EQ(node::GetCurrentEnvironment(context), nullptr);
  EXPECT_EQ(node::Environment::GetCurrent(isolate_), nullptr);

  v8::Context::Scope context_scope(context);
  EXPECT_EQ(node::Environment::GetCurrent(context), nullptr);
  EXPECT_EQ(node::GetCurrentEnvironment(context), nullptr);
  EXPECT_EQ(node::Environment::GetCurrent(isolate_), nullptr);
}

TEST_F(EnvironmentTest, NonNodeJSContext) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env test_env {handle_scope, argv};

  EXPECT_EQ(node::Environment::GetCurrent(v8::Local<v8::Context>()), nullptr);

  node::Environment* env = *test_env;
  EXPECT_EQ(node::Environment::GetCurrent(isolate_), env);
  EXPECT_EQ(node::Environment::GetCurrent(env->context()), env);
  EXPECT_EQ(node::GetCurrentEnvironment(env->context()), env);

  v8::Local<v8::Context> context = v8::Context::New(isolate_);
  EXPECT_EQ(node::Environment::GetCurrent(context), nullptr);
  EXPECT_EQ(node::GetCurrentEnvironment(context), nullptr);
  EXPECT_EQ(node::Environment::GetCurrent(isolate_), env);

  v8::Context::Scope context_scope(context);
  EXPECT_EQ(node::Environment::GetCurrent(context), nullptr);
  EXPECT_EQ(node::GetCurrentEnvironment(context), nullptr);
  EXPECT_EQ(node::Environment::GetCurrent(isolate_), nullptr);
}

static void at_exit_callback1(void* arg) {
  called_cb_1 = true;
  if (arg) {
    cb_1_arg = *static_cast<std::string*>(arg);
  }
}

static void at_exit_callback2(void* arg) {
  called_cb_2 = true;
}

static void at_exit_callback_ordered1(void* arg) {
  EXPECT_TRUE(called_cb_ordered_2);
  called_cb_ordered_1 = true;
}

static void at_exit_callback_ordered2(void* arg) {
  EXPECT_FALSE(called_cb_ordered_1);
  called_cb_ordered_2 = true;
}

static void at_exit_js(void* arg) {
  v8::Isolate* isolate = static_cast<v8::Isolate*>(arg);
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Object> obj = v8::Object::New(isolate);
  EXPECT_FALSE(obj.IsEmpty());  // Assert VM is still alive.
  EXPECT_TRUE(obj->IsObject());
  called_at_exit_js = true;
}

// Reproduces the sequence node::ObjectWrap performs since
// https://github.com/nodejs/node/pull/63642, without using ObjectWrap
// itself: the hook removes its own environment cleanup hook. When that runs
// while the cleanup queue is being drained, CleanupHookThunkRun() must not
// read the CleanupHookThunk after invoking the hook -- the hook has already
// erased and freed it. See https://github.com/nodejs/node/issues/65195.
static void self_removing_cleanup_hook(void* arg) {
  auto* state = static_cast<SelfRemovingCleanupHookState*>(arg);
  state->ran = true;
  node::RemoveEnvironmentCleanupHook(
      state->isolate, self_removing_cleanup_hook, state);
}

TEST_F(EnvironmentTest, SetImmediateCleanup) {
  int called = 0;
  int called_unref = 0;

  {
    const v8::HandleScope handle_scope(isolate_);
    const Argv argv;
    Env env {handle_scope, argv};

    node::LoadEnvironment(*env,
                          [&](const node::StartExecutionCallbackInfo& info)
                              -> v8::MaybeLocal<v8::Value> {
      return v8::Object::New(isolate_);
    });

    (*env)->SetImmediate([&](node::Environment* env_arg) {
      EXPECT_EQ(env_arg, *env);
      called++;
    }, node::CallbackFlags::kRefed);
    (*env)->SetImmediate([&](node::Environment* env_arg) {
      EXPECT_EQ(env_arg, *env);
      called_unref++;
    }, node::CallbackFlags::kUnrefed);
  }

  EXPECT_EQ(called, 1);
  EXPECT_EQ(called_unref, 0);
}

TEST_F(EnvironmentTest, RunAndClearNativeImmediatesSkipsEmptyScope) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};
  v8::Local<v8::Context> context = env.context();

  using IntVec = std::vector<int>;
  IntVec callback_calls;
  v8::Local<v8::Function> must_call =
      v8::Function::New(
          context,
          [](const v8::FunctionCallbackInfo<v8::Value>& info) {
            IntVec* callback_calls =
                static_cast<IntVec*>(info.Data().As<v8::External>()->Value(
                    v8::kExternalPointerTypeTagDefault));
            callback_calls->push_back(info[0].As<v8::Int32>()->Value());
          },
          v8::External::New(isolate_,
                            static_cast<void*>(&callback_calls),
                            v8::kExternalPointerTypeTagDefault))
          .ToLocalChecked();
  context->Global()
      ->Set(context,
            v8::String::NewFromUtf8Literal(isolate_, "mustCall"),
            must_call)
      .Check();

  v8::Local<v8::Function> eval_in_env =
      node::LoadEnvironment(*env, "return eval;")
          .ToLocalChecked()
          .As<v8::Function>();

  v8::Local<v8::Value> queue_microtask = v8::String::NewFromUtf8Literal(
      isolate_, "Promise.resolve().then(() => mustCall(1));");
  eval_in_env->Call(context, v8::Null(isolate_), 1, &queue_microtask)
      .ToLocalChecked();
  EXPECT_TRUE(callback_calls.empty());
  (*env)->RunAndClearNativeImmediates();
  EXPECT_TRUE(callback_calls.empty());

  context->GetMicrotaskQueue()->PerformCheckpoint(isolate_);
  EXPECT_EQ(callback_calls, (IntVec{1}));
  callback_calls.clear();

  queue_microtask = v8::String::NewFromUtf8Literal(
      isolate_, "Promise.resolve().then(() => mustCall(2));");
  eval_in_env->Call(context, v8::Null(isolate_), 1, &queue_microtask)
      .ToLocalChecked();
  EXPECT_TRUE(callback_calls.empty());

  bool native_immediate_called = false;
  (*env)->SetImmediate(
      [&](node::Environment* env_arg) {
        EXPECT_EQ(env_arg, *env);
        native_immediate_called = true;
      },
      node::CallbackFlags::kRefed);

  (*env)->RunAndClearNativeImmediates();
  EXPECT_TRUE(native_immediate_called);
  EXPECT_EQ(callback_calls, (IntVec{2}));
}

static char hello[] = "hello";

TEST_F(EnvironmentTest, BufferWithFreeCallbackIsDetached) {
  // Test that a Buffer allocated with a free callback is detached after
  // its callback has been called.
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;

  int callback_calls = 0;

  v8::Local<v8::ArrayBuffer> ab;
  {
    Env env {handle_scope, argv};
    v8::Local<v8::Object> buf_obj = node::Buffer::New(
        isolate_,
        hello,
        sizeof(hello),
        [](char* data, void* hint) {
          CHECK_EQ(data, hello);
          ++*static_cast<int*>(hint);
        },
        &callback_calls).ToLocalChecked();
    CHECK(buf_obj->IsUint8Array());
    ab = buf_obj.As<v8::Uint8Array>()->Buffer();
    CHECK_EQ(ab->ByteLength(), sizeof(hello));
  }

  CHECK_EQ(callback_calls, 1);
#ifdef V8_ENABLE_SANDBOX
  CHECK_EQ(ab->ByteLength(), sizeof(hello));
#else
  CHECK_EQ(ab->ByteLength(), 0);
#endif
}

#if HAVE_INSPECTOR
TEST_F(EnvironmentTest, InspectorMultipleEmbeddedEnvironments) {
  // Tests that child Environments can be created through the public API
  // that are accessible by the inspector.
  // This test sets a global variable in the child Environment, and reads it
  // back both through the inspector and inside the child Environment, and
  // makes sure that those correspond to the value that was originally set.
  v8::Locker locker(isolate_);
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env {handle_scope, argv};

  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
  node::LoadEnvironment(*env,
      "'use strict';\n"
      "const { Worker } = require('worker_threads');\n"
      "const { Session } = require('inspector');\n"

      "const session = new Session();\n"
      "session.connect();\n"
      "session.on('NodeWorker.attachedToWorker', (\n"
      "  ({ params: { workerInfo, sessionId } }) => {\n"
      "    session.post('NodeWorker.sendMessageToWorker', {\n"
      "      sessionId,\n"
      "      message: JSON.stringify({\n"
      "        id: 1,\n"
      "        method: 'Runtime.evaluate',\n"
      "        params: {\n"
      "          expression: 'globalThis.variableFromParent = 42;'\n"
      "        }\n"
      "      })\n"
      "    });\n"
      "    session.on('NodeWorker.receivedMessageFromWorker',\n"
      "      ({ params: { message } }) => {\n"
      "        global.messageFromWorker = \n"
      "          JSON.parse(message).result.result.value;\n"
      "      });\n"
      "  }));\n"
      "session.post('NodeWorker.enable', { waitForDebuggerOnStart: false });\n")
          .ToLocalChecked();

  struct ChildEnvironmentData {
    node::ThreadId thread_id;
    std::unique_ptr<node::InspectorParentHandle> inspector_parent_handle;
    node::MultiIsolatePlatform* platform;
    int32_t extracted_value = -1;
    uv_async_t thread_stopped_async;
  };

  ChildEnvironmentData data;
  data.thread_id = node::AllocateEnvironmentThreadId();
  data.inspector_parent_handle =
      GetInspectorParentHandle(*env, data.thread_id, "file:///embedded.js");
  CHECK(data.inspector_parent_handle);
  data.platform = GetMultiIsolatePlatform(*env);
  CHECK_NOT_NULL(data.platform);

  bool thread_stopped = false;
  int err = uv_async_init(
      &current_loop, &data.thread_stopped_async, [](uv_async_t* async) {
        *static_cast<bool*>(async->data) = true;
        uv_close(reinterpret_cast<uv_handle_t*>(async), nullptr);
      });
  CHECK_EQ(err, 0);
  data.thread_stopped_async.data = &thread_stopped;

  uv_thread_t thread;
  err = uv_thread_create(&thread, [](void* arg) {
    ChildEnvironmentData* data = static_cast<ChildEnvironmentData*>(arg);
    std::shared_ptr<node::ArrayBufferAllocator> aba =
        node::ArrayBufferAllocator::Create();
    uv_loop_t loop;
    uv_loop_init(&loop);
    v8::Isolate* isolate = NewIsolate(aba, &loop, data->platform);
    CHECK_NOT_NULL(isolate);

    {
      v8::Locker locker(isolate);
      v8::Isolate::Scope isolate_scope(isolate);
      v8::HandleScope handle_scope(isolate);

      v8::Local<v8::Context> context = node::NewContext(isolate);
      CHECK(!context.IsEmpty());
      v8::Context::Scope context_scope(context);

      node::IsolateData* isolate_data = node::CreateIsolateData(
          isolate,
          &loop,
          data->platform);
      CHECK_NOT_NULL(isolate_data);
      node::Environment* environment = node::CreateEnvironment(
          isolate_data,
          context,
          { "dummy" },
          {},
          node::EnvironmentFlags::kNoFlags,
          data->thread_id,
          std::move(data->inspector_parent_handle));
      CHECK_NOT_NULL(environment);

      v8::Local<v8::Value> extracted_value = LoadEnvironment(
          environment,
          "while (!global.variableFromParent) {}\n"
          "return global.variableFromParent;").ToLocalChecked();

      uv_run(&loop, UV_RUN_DEFAULT);
      CHECK(extracted_value->IsInt32());
      data->extracted_value = extracted_value.As<v8::Int32>()->Value();

      node::FreeEnvironment(environment);
      node::FreeIsolateData(isolate_data);
    }

    data->platform->DisposeIsolate(isolate);
    uv_run(&loop, UV_RUN_DEFAULT);
    CHECK_EQ(uv_loop_close(&loop), 0);

    uv_async_send(&data->thread_stopped_async);
  }, &data);
  CHECK_EQ(err, 0);

  bool more;
  do {
    uv_run(&current_loop, UV_RUN_DEFAULT);
    data.platform->DrainTasks(isolate_);
    more = uv_loop_alive(&current_loop);
  } while (!thread_stopped || more);

  uv_thread_join(&thread);

  v8::Local<v8::Value> from_inspector =
      context->Global()->Get(
          context,
          v8::String::NewFromOneByte(
              isolate_,
              reinterpret_cast<const uint8_t*>("messageFromWorker"))
              .ToLocalChecked())
              .ToLocalChecked();
  CHECK_EQ(data.extracted_value, 42);
  CHECK_EQ(from_inspector->IntegerValue(context).FromJust(), 42);
}

TEST_F(EnvironmentTest, InspectorWithoutPlatform) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  node::IsolateData* isolate_data = node::CreateIsolateData(
      isolate_, &NodeTestFixture::current_loop, nullptr);
  v8::Local<v8::Context> context = node::NewContext(isolate_);
  v8::Context::Scope context_scope(context);
  std::vector<std::string> args(*argv, *argv + 1);
  node::Environment* env =
      node::CreateEnvironment(isolate_data, context, args, args);
  CHECK_NOT_NULL(env);

  v8::Local<v8::Value> result =
      node::LoadEnvironment(env,
                            "const { Session } = require('inspector');\n"
                            "const session = new Session();\n"
                            "session.connect();\n"
                            "console.time('t'); console.timeEnd('t');\n"
                            "session.disconnect();\n"
                            "return 42;")
          .ToLocalChecked();
  EXPECT_EQ(result->Int32Value(context).FromJust(), 42);

  node::FreeEnvironment(env);
  node::FreeIsolateData(isolate_data);
}

#endif  // HAVE_INSPECTOR

TEST_F(EnvironmentTest, ExitHandlerTest) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;

  int callback_calls = 0;

  Env env {handle_scope, argv};
  SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) {
    EXPECT_EQ(*env, env_);
    EXPECT_EQ(exit_code, 42);
    callback_calls++;
    node::Stop(*env);
  });
  // When terminating, v8 throws makes the current embedder call bail out
  // with MaybeLocal<>()
  EXPECT_TRUE(node::LoadEnvironment(*env, "process.exit(42)").IsEmpty());
  EXPECT_EQ(callback_calls, 1);
}

TEST_F(EnvironmentTest, SetImmediateMicrotasks) {
  int called = 0;

  {
    const v8::HandleScope handle_scope(isolate_);
    const Argv argv;
    Env env {handle_scope, argv};

    node::LoadEnvironment(*env,
                          [&](const node::StartExecutionCallbackInfo& info)
                              -> v8::MaybeLocal<v8::Value> {
      return v8::Object::New(isolate_);
    });

    (*env)->SetImmediate([&](node::Environment* env_arg) {
      EXPECT_EQ(env_arg, *env);
      isolate_->EnqueueMicrotask([](void* arg) {
        ++*static_cast<int*>(arg);
      }, &called);
    }, node::CallbackFlags::kRefed);
    uv_run(&current_loop, UV_RUN_DEFAULT);
  }

  EXPECT_EQ(called, 1);
}

#ifndef _WIN32  // No SIGINT on Windows.
TEST_F(NodeZeroIsolateTestFixture, CtrlCWithOnlySafeTerminationTest) {
  // Allocate and initialize Isolate.
  v8::Isolate::CreateParams create_params;
  create_params.array_buffer_allocator = allocator.get();
  create_params.cpp_heap =
      v8::CppHeap::Create(platform.get(), v8::CppHeapCreateParams{{}})
          .release();
  v8::Isolate* isolate = v8::Isolate::Allocate();
  CHECK_NOT_NULL(isolate);
  platform->RegisterIsolate(isolate, &current_loop);
  v8::Isolate::Initialize(isolate, create_params);

  // Try creating Context + IsolateData + Environment.
  {
    v8::Locker locker(isolate);
    v8::Isolate::Scope isolate_scope(isolate);
    v8::HandleScope handle_scope(isolate);

    auto context = node::NewContext(isolate);
    CHECK(!context.IsEmpty());
    v8::Context::Scope context_scope(context);

    std::unique_ptr<node::IsolateData, decltype(&node::FreeIsolateData)>
      isolate_data{node::CreateIsolateData(isolate,
                                           &current_loop,
                                           platform.get()),
                   node::FreeIsolateData};
    CHECK(isolate_data);

    std::unique_ptr<node::Environment, decltype(&node::FreeEnvironment)>
      environment{node::CreateEnvironment(isolate_data.get(),
                                          context,
                                          {},
                                          {}),
                  node::FreeEnvironment};
    CHECK(environment);
    EXPECT_EQ(node::GetEnvironmentIsolateData(environment.get()),
              isolate_data.get());
    EXPECT_EQ(node::GetArrayBufferAllocator(isolate_data.get()), nullptr);

    v8::Local<v8::Value> main_ret =
        node::LoadEnvironment(environment.get(),
            "'use strict';\n"
            "const { runInThisContext } = require('vm');\n"
            "try {\n"
            "  runInThisContext("
            "    `process.kill(process.pid, 'SIGINT'); while(true){}`, "
            "    { breakOnSigint: true });\n"
            "  return 'unreachable';\n"
            "} catch (err) {\n"
            "  return err.code;\n"
            "}").ToLocalChecked();
    node::Utf8Value main_ret_str(isolate, main_ret);
    EXPECT_EQ(std::string(*main_ret_str), "ERR_SCRIPT_EXECUTION_INTERRUPTED");
  }

  // Cleanup.
  platform->DisposeIsolate(isolate);
}
#endif  // _WIN32

TEST_F(EnvironmentTest, NestedMicrotaskQueue) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;

  v8::MicrotaskQueue* queue =
      v8::MicrotaskQueue::New(isolate_, v8::MicrotasksPolicy::kExplicit);

  v8::Local<v8::Context> context =
      v8::Context::New(isolate_,
                       nullptr,
                       {},
                       {},
                       v8::DeserializeInternalFieldsCallback(),
                       queue);
  node::InitializeContext(context);
  v8::Context::Scope context_scope(context);

  using IntVec = std::vector<int>;
  IntVec callback_calls;
  v8::Local<v8::Function> must_call =
      v8::Function::New(
          context,
          [](const v8::FunctionCallbackInfo<v8::Value>& info) {
            IntVec* callback_calls =
                static_cast<IntVec*>(info.Data().As<v8::External>()->Value(
                    v8::kExternalPointerTypeTagDefault));
            callback_calls->push_back(info[0].As<v8::Int32>()->Value());
          },
          v8::External::New(isolate_,
                            static_cast<void*>(&callback_calls),
                            v8::kExternalPointerTypeTagDefault))
          .ToLocalChecked();
  context->Global()->Set(
      context,
      v8::String::NewFromUtf8Literal(isolate_, "mustCall"),
      must_call).Check();

  node::Environment* env =
      node::CreateEnvironment(isolate_data_, context, {}, {});
  CHECK_NE(nullptr, env);

  v8::Local<v8::Function> eval_in_env = node::LoadEnvironment(
      env,
      "mustCall(1);\n"
      "Promise.resolve().then(() => mustCall(2));\n"
      "require('vm').runInNewContext("
      "    'Promise.resolve().then(() => mustCall(3))',"
      "    { mustCall },"
      "    { microtaskMode: 'afterEvaluate' }"
      ");\n"
      "require('vm').runInNewContext("
      "    'Promise.resolve().then(() => mustCall(4))',"
      "    { mustCall }"
      ");\n"
      "setTimeout(() => {"
      "  Promise.resolve().then(() => mustCall(5));"
      "}, 10);\n"
      "mustCall(6);\n"
      "return eval;\n").ToLocalChecked().As<v8::Function>();
  EXPECT_EQ(callback_calls, (IntVec { 1, 3, 6, 2, 4 }));
  v8::Local<v8::Value> queue_microtask_code = v8::String::NewFromUtf8Literal(
      isolate_, "queueMicrotask(() => mustCall(7));");
  eval_in_env->Call(context,
                    v8::Null(isolate_),
                    1,
                    &queue_microtask_code).ToLocalChecked();
  EXPECT_EQ(callback_calls, (IntVec { 1, 3, 6, 2, 4 }));
  isolate_->PerformMicrotaskCheckpoint();
  EXPECT_EQ(callback_calls, (IntVec { 1, 3, 6, 2, 4 }));
  queue->PerformCheckpoint(isolate_);
  EXPECT_EQ(callback_calls, (IntVec { 1, 3, 6, 2, 4, 7 }));

  int exit_code = SpinEventLoop(env).FromJust();
  EXPECT_EQ(exit_code, 0);
  EXPECT_EQ(callback_calls, (IntVec { 1, 3, 6, 2, 4, 7, 5 }));

  node::FreeEnvironment(env);
}

static bool interrupted = false;
static void OnInterrupt(void* arg) {
  interrupted = true;
}
TEST_F(EnvironmentTest, RequestInterruptAtExit) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;

  Local<Context> context = node::NewContext(isolate_);
  CHECK(!context.IsEmpty());
  context->Enter();

  std::vector<std::string> args(*argv, *argv + 1);
  std::vector<std::string> exec_args(*argv, *argv + 1);
  node::Environment* environment =
      node::CreateEnvironment(isolate_data_, context, args, exec_args);
  CHECK_NE(nullptr, environment);

  node::RequestInterrupt(environment, OnInterrupt, nullptr);
  node::FreeEnvironment(environment);
  EXPECT_TRUE(interrupted);

  context->Exit();
}

TEST_F(EnvironmentTest, EmbedderBuiltinCodeCache) {
  v8::HandleScope handle_scope(isolate_);
  v8::Local<v8::Context> context = node::NewContext(isolate_);
  v8::Context::Scope context_scope(context);

  std::vector<node::EmbedderBuiltinCodeCache::Entry> entries =
      node::EmbedderBuiltinCodeCache::Generate(context);
  ASSERT_GT(entries.size(), 100u);
  {
    node::EmbedderBuiltinCodeCache cache(std::move(entries));
    EXPECT_EQ(node::SetBuiltinCodeCache(isolate_data_, &cache),
              v8::ScriptCompiler::CachedData::kSuccess);
  }
  std::unique_ptr<node::Environment, decltype(&node::FreeEnvironment)> env(
      node::CreateEnvironment(isolate_data_, context, {}, {}),
      node::FreeEnvironment);
  node::Realm* realm = env->principal_realm();
  EXPECT_EQ(realm->builtins_with_cache.count("internal/bootstrap/node"), 1u);
  for (const std::string& id : realm->builtins_without_cache) {
    EXPECT_EQ(id.rfind("internal/per_context/", 0), 0u) << id;
  }

  uint8_t* bytes = new uint8_t[64]();
  std::vector<node::EmbedderBuiltinCodeCache::Entry> bad;
  bad.push_back({"internal/bootstrap/node",
                 std::make_unique<v8::ScriptCompiler::CachedData>(
                     bytes, 64, v8::ScriptCompiler::CachedData::BufferOwned)});
  node::EmbedderBuiltinCodeCache bad_cache(std::move(bad));
  EXPECT_NE(node::SetBuiltinCodeCache(isolate_data_, &bad_cache),
            v8::ScriptCompiler::CachedData::kSuccess);
  EXPECT_FALSE(isolate_data_->builtin_code_cache().empty());
  node::SetBuiltinCodeCache(isolate_data_, nullptr);
  EXPECT_TRUE(isolate_data_->builtin_code_cache().empty());
}

TEST_F(EnvironmentTest, EmbedderPreload) {
  v8::HandleScope handle_scope(isolate_);
  v8::Local<v8::Context> context = node::NewContext(isolate_);
  v8::Context::Scope context_scope(context);

  node::EmbedderPreloadCallback preload = [](node::Environment* env,
                                             v8::Local<v8::Value> process,
                                             v8::Local<v8::Value> require) {
    CHECK(process->IsObject());
    CHECK(require->IsFunction());
    process.As<v8::Object>()
        ->Set(env->context(),
              v8::String::NewFromUtf8Literal(env->isolate(), "prop"),
              v8::String::NewFromUtf8Literal(env->isolate(), "preload"))
        .Check();
  };

  std::unique_ptr<node::Environment, decltype(&node::FreeEnvironment)> env(
      node::CreateEnvironment(isolate_data_, context, {}, {}),
      node::FreeEnvironment);

  v8::Local<v8::Value> main_ret =
      node::LoadEnvironment(env.get(), "return process.prop;", preload)
          .ToLocalChecked();
  node::Utf8Value main_ret_str(isolate_, main_ret);
  EXPECT_EQ(std::string(*main_ret_str), "preload");
}

TEST_F(EnvironmentTest, LoadEnvironmentWithESModule) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};

  node::ModuleData entry_point;
  std::string source =
      "import importedProcess from 'process';\n"
      "export const process = importedProcess;\n"
      "const stack = new Error().stack.split('\\n');\n"
      "export const frame = stack.filter(line => /at /.test(line))[0];\n";
  entry_point.set_source(source);
  entry_point.set_format(node::ModuleFormat::kModule);
  entry_point.set_resource_name("embedded:esm.mjs");

  v8::Local<v8::Value> result =
      node::LoadEnvironment(*env, &entry_point).ToLocalChecked();

  // The ESM entry point returns the module namespace object.
  EXPECT_TRUE(result->IsObject());
  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
  v8::Local<v8::String> process_str =
      v8::String::NewFromUtf8Literal(isolate_, "process");
  v8::Local<v8::Value> process_value =
      result.As<v8::Object>()->Get(context, process_str).ToLocalChecked();
  EXPECT_TRUE(process_value->IsObject());
  v8::Local<v8::Value> global_process_value =
      context->Global()->Get(context, process_str).ToLocalChecked();
  EXPECT_TRUE(global_process_value->IsObject());
  EXPECT_TRUE(process_value->StrictEquals(global_process_value));

  v8::Local<v8::Value> frame_value =
      result.As<v8::Object>()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "frame"))
          .ToLocalChecked();
  EXPECT_TRUE(frame_value->IsString());
  node::Utf8Value frame_str(isolate_, frame_value);
  printf("Frame: %s\n", *frame_str);
  EXPECT_EQ(frame_str.ToString(), "    at embedded:esm.mjs:3:15");
}

static const char* dynamic_import_source =
    "const importedProcess = import('node:process');\n"
    "const importedNonBuiltin = import('./non-existent.mjs');\n"
    "importedNonBuiltin.catch((err) => \n"
    "  globalThis.importedNonBuiltinStack = err.stack)\n";
void CheckDynamicImportResult(v8::Isolate* isolate,
                              v8::Local<v8::Value> result) {
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  // Check the result of import('node:process').
  EXPECT_TRUE(result->IsObject());
  v8::Local<v8::Value> imported_process =
      result.As<v8::Object>()
          ->Get(context,
                v8::String::NewFromUtf8Literal(isolate, "importedProcess"))
          .ToLocalChecked();
  EXPECT_TRUE(imported_process->IsPromise());
  EXPECT_EQ(imported_process.As<v8::Promise>()->State(),
            v8::Promise::kFulfilled);
  v8::Local<v8::Value> imported_value =
      imported_process.As<v8::Promise>()->Result();
  EXPECT_TRUE(imported_value->IsObject());
  v8::Local<v8::Value> dynamic_process =
      imported_value.As<v8::Object>()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate, "default"))
          .ToLocalChecked();
  EXPECT_TRUE(dynamic_process->IsObject());

  v8::Local<v8::Value> global_process_value =
      context->Global()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate, "process"))
          .ToLocalChecked();
  EXPECT_TRUE(global_process_value->IsObject());
  EXPECT_TRUE(dynamic_process->StrictEquals(global_process_value));

  // Check the result of import('./non-existent.mjs')
  v8::Local<v8::Value> imported_non_builtin =
      result.As<v8::Object>()
          ->Get(context,
                v8::String::NewFromUtf8Literal(isolate, "importedNonBuiltin"))
          .ToLocalChecked();
  EXPECT_TRUE(imported_non_builtin->IsPromise());
  EXPECT_EQ(imported_non_builtin.As<v8::Promise>()->State(),
            v8::Promise::kRejected);

  // Check the error message
  v8::Local<v8::Value> imported_non_builtin_stack =
      context->Global()
          ->Get(context,
                v8::String::NewFromUtf8Literal(isolate,
                                               "importedNonBuiltinStack"))
          .ToLocalChecked();
  EXPECT_TRUE(imported_non_builtin_stack->IsString());
  node::Utf8Value error_str(isolate, imported_non_builtin_stack);
  // Error stack should include "ERR_UNKNOWN_BUILTIN_MODULE"
  EXPECT_NE(error_str.ToString().find("ERR_UNKNOWN_BUILTIN_MODULE"),
            std::string::npos);
  // Error stack should include "No such built-in module: ./non-existent.mjs"
  EXPECT_NE(
      error_str.ToString().find("No such built-in module: ./non-existent.mjs"),
      std::string::npos);
}

TEST_F(EnvironmentTest, LoadEnvironmentWithESModuleDynamicImport) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};

  // Test dynamic import('node:process') in entry point.
  std::string source = std::string(dynamic_import_source) +
                       "export { importedProcess, importedNonBuiltin };\n";
  node::ModuleData entry_point;
  entry_point.set_source(source);
  entry_point.set_format(node::ModuleFormat::kModule);
  entry_point.set_resource_name("embedded:dynamic-import.mjs");

  v8::Local<v8::Value> result =
      node::LoadEnvironment(*env, &entry_point).ToLocalChecked();

  // Finish the await.
  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
  context->GetMicrotaskQueue()->PerformCheckpoint(isolate_);
  CheckDynamicImportResult(isolate_, result);
}

TEST_F(EnvironmentTest, LoadEnvironmentWithCommonJSDynamicImport) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};

  std::string source = std::string(dynamic_import_source) +
                       "return { importedProcess, importedNonBuiltin };\n";
  node::ModuleData entry_point;
  entry_point.set_source(source);
  entry_point.set_format(node::ModuleFormat::kCommonJS);
  entry_point.set_resource_name("/test-cjs-dynamic-import.js");

  v8::Local<v8::Value> result =
      node::LoadEnvironment(*env, &entry_point).ToLocalChecked();

  // Finish the await.
  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
  context->GetMicrotaskQueue()->PerformCheckpoint(isolate_);
  CheckDynamicImportResult(isolate_, result);
}

TEST_F(EnvironmentTest, LoadEnvironmentWithESModuleImportMeta) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};

  node::ModuleData entry_point;
  // Test import.meta properties in ESM entry point.
  std::string source = "export const url = import.meta.url;\n"
                       "export const main = import.meta.main;\n"
                       "export const filename = import.meta.filename;\n"
                       "export const dirname = import.meta.dirname;\n";
  std::string exec_path = (*env)->exec_path();
  std::string url = node::url::FromFilePath(exec_path);
  entry_point.set_source(source);
  entry_point.set_format(node::ModuleFormat::kModule);
  entry_point.set_resource_name(url);

  v8::Local<v8::Value> result =
      node::LoadEnvironment(*env, &entry_point).ToLocalChecked();

  EXPECT_TRUE(result->IsObject());
  v8::Local<v8::Context> context = isolate_->GetCurrentContext();

  // Check import.meta.url
  v8::Local<v8::Value> url_value =
      result.As<v8::Object>()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "url"))
          .ToLocalChecked();
  EXPECT_TRUE(url_value->IsString());
  node::Utf8Value url_str(isolate_, url_value);
  EXPECT_EQ(url_str.ToStringView(), url);

  // Check import.meta.main
  v8::Local<v8::Value> main_value =
      result.As<v8::Object>()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "main"))
          .ToLocalChecked();
  EXPECT_TRUE(main_value->IsBoolean());
  EXPECT_TRUE(main_value->BooleanValue(isolate_));

  // Check import.meta.filename
  v8::Local<v8::Value> filename_value =
      result.As<v8::Object>()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "filename"))
          .ToLocalChecked();
  EXPECT_TRUE(filename_value->IsString());
  node::Utf8Value filename_str(isolate_, filename_value);
  EXPECT_EQ(filename_str.ToStringView(), exec_path);

  // Check import.meta.dirname
  v8::Local<v8::Value> dirname_value =
      result.As<v8::Object>()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "dirname"))
          .ToLocalChecked();
  EXPECT_TRUE(dirname_value->IsString());
  node::Utf8Value dirname_str(isolate_, dirname_value);
  // Just check that dirname is a substring of exec_path
  EXPECT_NE(exec_path.find(dirname_str.ToStringView()), std::string::npos);
}

TEST_F(EnvironmentTest, LoadEnvironmentWithCallbackWithCommonJSModule) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};

  v8::Local<v8::Context> context = isolate_->GetCurrentContext();

  v8::MaybeLocal<v8::Value> maybe = node::LoadEnvironment(
      *env,
      [&](const node::StartExecutionCallbackInfoWithModule& info)
          -> v8::MaybeLocal<v8::Value> {
        EXPECT_NE(nullptr, info.env());
        EXPECT_TRUE(info.process_object()->IsObject());
        EXPECT_TRUE(info.native_require()->IsFunction());
        EXPECT_TRUE(info.run_module()->IsFunction());

        // require("process") === process
        v8::Local<v8::String> process_str =
            v8::String::NewFromUtf8Literal(isolate_, "process");
        v8::Local<v8::Value> require_args[] = {process_str};
        v8::Local<v8::Value> required_process =
            info.native_require()
                ->Call(context, v8::Null(isolate_), 1, require_args)
                .ToLocalChecked();
        EXPECT_TRUE(required_process->IsObject());
        EXPECT_TRUE(required_process->StrictEquals(info.process_object()));

        // Test running a CJS entry point via run_module.
        v8::Local<v8::Value> cjs_source = v8::String::NewFromUtf8Literal(
            isolate_,
            "globalThis.processInModule = require('process');\n"
            "const stack = new Error().stack.split('\\n');\n"
            "globalThis.frame = stack.filter(line => /at /.test(line))[0];\n"
            "return 42;\n");
        v8::Local<v8::Value> format = v8::Integer::New(
            isolate_, static_cast<int32_t>(node::ModuleFormat::kCommonJS));
        v8::Local<v8::Value> resource_name =
            v8::String::NewFromUtf8Literal(isolate_, "/test-cjs.js");
        v8::Local<v8::Value> args[] = {cjs_source, format, resource_name};
        v8::Local<v8::Value> result =
            info.run_module()
                ->Call(context, v8::Null(isolate_), 3, args)
                .ToLocalChecked();
        EXPECT_TRUE(result->IsUint32());
        EXPECT_EQ(result.As<v8::Uint32>()->Value(), static_cast<uint32_t>(42));
        v8::Local<v8::Value> process_in_module =
            context->Global()
                ->Get(
                    context,
                    v8::String::NewFromUtf8Literal(isolate_, "processInModule"))
                .ToLocalChecked();
        EXPECT_TRUE(process_in_module->IsObject());
        EXPECT_TRUE(process_in_module->StrictEquals(info.process_object()));
        return result;
      },
      nullptr);

  EXPECT_TRUE(!maybe.IsEmpty());
  v8::Local<v8::Value> frame_value =
      context->Global()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "frame"))
          .ToLocalChecked();
  EXPECT_TRUE(frame_value->IsString());
  node::Utf8Value frame_str(isolate_, frame_value);
  printf("Frame: %s\n", *frame_str);
  EXPECT_EQ(frame_str.ToString(), "    at /test-cjs.js:2:15");
}

TEST_F(EnvironmentTest, LoadEnvironmentWithCallbackWithESModule) {
  const v8::HandleScope handle_scope(isolate_);
  const Argv argv;
  Env env{handle_scope, argv};

  v8::Local<v8::Context> context = isolate_->GetCurrentContext();

  v8::MaybeLocal<v8::Value> maybe = node::LoadEnvironment(
      *env,
      [&](const node::StartExecutionCallbackInfoWithModule& info)
          -> v8::MaybeLocal<v8::Value> {
        // Test running an ESM entry point via run_module.
        v8::Local<v8::Value> source_str = v8::String::NewFromUtf8Literal(
            isolate_,
            "import importedProcess from 'process';\n"
            "export const process = importedProcess;\n"
            "const stack = new Error().stack.split('\\n');\n"
            "export const frame = stack.filter(line => /at "
            "/.test(line))[0];\n");
        v8::Local<v8::Value> format = v8::Integer::New(
            isolate_, static_cast<int32_t>(node::ModuleFormat::kModule));
        v8::Local<v8::Value> resource_name =
            v8::String::NewFromUtf8Literal(isolate_, "embedded:esm.mjs");
        v8::Local<v8::Value> args[] = {source_str, format, resource_name};
        return info.run_module()->Call(context, v8::Null(isolate_), 3, args);
      },
      nullptr);

  v8::Local<v8::Value> result = maybe.ToLocalChecked();

  v8::Local<v8::String> process_str =
      v8::String::NewFromUtf8Literal(isolate_, "process");
  // The ESM entry point returns the module namespace object.
  EXPECT_TRUE(result->IsObject());
  v8::Local<v8::Value> process_value =
      result.As<v8::Object>()->Get(context, process_str).ToLocalChecked();
  EXPECT_TRUE(process_value->IsObject());
  v8::Local<v8::Value> global_process_value =
      context->Global()->Get(context, process_str).ToLocalChecked();
  EXPECT_TRUE(global_process_value->IsObject());
  EXPECT_TRUE(process_value->StrictEquals(global_process_value));

  v8::Local<v8::Value> frame_value =
      result.As<v8::Object>()
          ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "frame"))
          .ToLocalChecked();
  EXPECT_TRUE(frame_value->IsString());
  node::Utf8Value frame_str(isolate_, frame_value);
  printf("Frame: %s\n", *frame_str);
  EXPECT_EQ(frame_str.ToString(), "    at embedded:esm.mjs:3:15");
}

namespace {
void CustomAbortHandlerForContractTest(const char* location,
                                       const char* message) {}

bool abort_handler_dispatch_flag = false;
const char* abort_handler_received_location = nullptr;
const char* abort_handler_received_message = nullptr;
void AbortHandlerThatSetsDispatchFlag(const char* location,
                                      const char* message) {
  abort_handler_dispatch_flag = true;
  abort_handler_received_location = location;
  abort_handler_received_message = message;
}
}  // namespace

TEST(AbortHandlerTest, DefaultIsNonNullAndSetAbortHandlerRoundTrips) {
  node::AbortHandler old = node::GetAbortHandler();

  // There should always be a non-null default handler installed.
  EXPECT_NE(node::GetAbortHandler(), nullptr);

  node::SetAbortHandler(CustomAbortHandlerForContractTest);
  EXPECT_EQ(node::GetAbortHandler(), CustomAbortHandlerForContractTest);

  node::SetAbortHandler(nullptr);
  EXPECT_NE(node::GetAbortHandler(), nullptr);
  EXPECT_NE(node::GetAbortHandler(), CustomAbortHandlerForContractTest);

  node::SetAbortHandler(old);
}

TEST(AbortHandlerTest, InstalledHandlerIsInvokedWhenCalled) {
  node::AbortHandler old = node::GetAbortHandler();
  abort_handler_dispatch_flag = false;
  abort_handler_received_location = nullptr;
  abort_handler_received_message = nullptr;

  node::SetAbortHandler(AbortHandlerThatSetsDispatchFlag);
  node::AbortHandler h = node::GetAbortHandler();
  // Fail cleanly (instead of crashing on a null call) if the handler wasn't
  // actually installed.
  ASSERT_NE(h, nullptr);

  // Dispatch through the public GetAbortHandler() accessor directly (not via
  // the ABORT() macro, so nothing terminates), and verify the message is
  // passed through unchanged.
  node::GetAbortHandler()("some-test-location", "some-test-message");
  EXPECT_TRUE(abort_handler_dispatch_flag);
  EXPECT_STREQ(abort_handler_received_location, "some-test-location");
  EXPECT_STREQ(abort_handler_received_message, "some-test-message");

  node::SetAbortHandler(old);
}