blob: 5e05577c8e1e5410d5523202aad8e7f7ffdc90c5 [file] [edit]
// Copyright 2026 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#include <gtest/gtest.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <optional>
#include <ostream>
#include <sstream>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include "src/dawn/common/SystemUtils.h"
#include "src/dawn/node/napi_v8/napi_v8.h"
#include "src/dawn/node/standalone/EventLoop.h"
#include "src/dawn/node/standalone/Polyfills.h"
#include "src/dawn/node/test/V8TestEnvironment.h"
namespace {
// How long the clock tests sleep, and the bounds a reading taken across that sleep must fall
// within. The lower bound sits under the sleep to leave room for clock-source rounding; the upper
// is loose enough that only a wrong unit, rather than a loaded machine, can reach it.
constexpr std::chrono::milliseconds kSleepDuration{50};
constexpr double kMinElapsedMs = 40.0;
constexpr double kMaxElapsedMs = 5000.0;
// Redirects `stream` into a buffer for the lifetime of this object, so that tests can assert on
// the exact bytes a polyfill writes rather than merely that it did not crash.
class StreamCapture {
public:
explicit StreamCapture(std::ostream& stream) : stream_(stream) {
original_ = stream_.rdbuf(buffer_.rdbuf());
}
~StreamCapture() { stream_.rdbuf(original_); }
StreamCapture(const StreamCapture&) = delete;
StreamCapture& operator=(const StreamCapture&) = delete;
std::string Str() const { return buffer_.str(); }
private:
std::ostringstream buffer_;
std::ostream& stream_;
std::streambuf* original_;
};
// A directory of its own for a test that needs real files, removed when the test ends. The name
// carries the test that asked for it, and a counter keeps two of them apart.
class TempDir {
public:
TempDir() {
static std::atomic<uint32_t> counter{0};
const ::testing::TestInfo* info = ::testing::UnitTest::GetInstance()->current_test_info();
std::filesystem::path base = std::filesystem::temp_directory_path();
for (uint32_t attempt = 0; attempt < kMaxAttempts; ++attempt) {
std::filesystem::path candidate =
base /
("dawn_polyfills_" + std::string(info->name()) + "_" + std::to_string(counter++));
std::error_code ec;
if (std::filesystem::create_directory(candidate, ec)) {
path_ = candidate;
return;
}
}
ADD_FAILURE() << "no temporary directory could be created under " << base;
}
~TempDir() {
std::error_code ec;
std::filesystem::remove_all(path_, ec);
}
TempDir(const TempDir&) = delete;
TempDir& operator=(const TempDir&) = delete;
// Paths handed to JavaScript use forward slashes throughout: Windows accepts them, and unlike
// a backslash they do not start an escape sequence when pasted into a string literal.
std::string JsPath() const { return path_.generic_string(); }
// Creates a file in the directory and returns its path, for JavaScript.
std::string WriteFile(const std::string& name, const std::string& contents) const {
std::filesystem::path file = path_ / name;
std::error_code ec;
std::filesystem::create_directories(file.parent_path(), ec);
std::ofstream out(file, std::ios::binary);
out << contents;
out.close();
EXPECT_TRUE(out.good()) << "could not write " << file;
return file.generic_string();
}
private:
static constexpr uint32_t kMaxAttempts = 32;
std::filesystem::path path_;
};
class PolyfillsTest : public dawn::node::test::V8IsolateTest {
protected:
PolyfillsTest() : V8IsolateTest(v8::MicrotasksPolicy::kExplicit) {}
void SetUp() override {
V8IsolateTest::SetUp();
env_ = dawn::napi_v8::CreateEnv(isolate_, context());
loop_.emplace(isolate_, dawn::node::test::V8Platform());
}
void TearDown() override {
// Before the environment: a task still queued in the loop owns a reference to the
// JavaScript callback it was going to call.
loop_.reset();
dawn::napi_v8::DestroyEnv(env_);
V8IsolateTest::TearDown();
}
// The loop the timer polyfills schedule onto. A test drives it by hand with RunOneIteration()
// rather than running it, so that nothing depends on how long anything takes.
dawn::node::standalone::EventLoop& loop() { return *loop_; }
napi_value RunScript(const std::string& code) {
napi_value script_src;
napi_value result;
napi_status status =
napi_create_string_utf8(env_, code.c_str(), code.length(), &script_src);
EXPECT_EQ(status, napi_ok);
status = napi_run_script(env_, script_src, &result);
EXPECT_EQ(status, napi_ok);
return result;
}
void RunScriptAllowingExit(const std::string& code) {
napi_value script_src;
napi_value result;
ASSERT_EQ(napi_create_string_utf8(env_, code.c_str(), code.length(), &script_src), napi_ok);
napi_run_script(env_, script_src, &result);
isolate_->CancelTerminateExecution();
}
void RunMicrotasks() { isolate_->PerformMicrotaskCheckpoint(); }
// Whether evaluating `expression` throws an Error. RunScript() expects the script itself to
// succeed, so the throw has to be caught in JavaScript rather than escaping to the fixture.
bool Throws(const std::string& expression) {
return ToBool(RunScript("(function() { try { " + expression +
"; return false; } catch (e) { return e instanceof Error; } })()"));
}
std::string ToString(napi_value value) {
size_t length = 0;
EXPECT_EQ(napi_get_value_string_utf8(env_, value, nullptr, 0, &length), napi_ok);
std::vector<char> buffer(length + 1);
EXPECT_EQ(napi_get_value_string_utf8(env_, value, buffer.data(), buffer.size(), &length),
napi_ok);
return std::string(buffer.data(), length);
}
bool ToBool(napi_value value) {
bool result = false;
EXPECT_EQ(napi_get_value_bool(env_, value, &result), napi_ok);
return result;
}
uint32_t ToUint32(napi_value value) {
uint32_t result = 0;
EXPECT_EQ(napi_get_value_uint32(env_, value, &result), napi_ok);
return result;
}
double ToDouble(napi_value value) {
double result = 0.0;
EXPECT_EQ(napi_get_value_double(env_, value, &result), napi_ok);
return result;
}
napi_env env_ = nullptr;
private:
// Held by value once SetUp() has an isolate to give it.
std::optional<dawn::node::standalone::EventLoop> loop_;
};
TEST_F(PolyfillsTest, ConsoleGlobals) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(
ToBool(RunScript("typeof console.log === 'function' && "
"typeof console.warn === 'function' && "
"typeof console.error === 'function' && "
"typeof console.info === 'function' && "
"typeof console.debug === 'function'")));
}
TEST_F(PolyfillsTest, ConsoleLogInfoAndDebugWriteLinesToStdout) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
StreamCapture out(std::cout);
StreamCapture log(std::clog);
StreamCapture err(std::cerr);
RunScript("console.log('one'); console.info('two'); console.debug('three');");
// All three are unprefixed stdout writes, one line each.
EXPECT_EQ(out.Str(), "one\ntwo\nthree\n");
EXPECT_EQ(log.Str(), "");
EXPECT_EQ(err.Str(), "");
}
TEST_F(PolyfillsTest, ConsoleWarnAndErrorAreLabelled) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
StreamCapture out(std::cout);
StreamCapture log(std::clog);
StreamCapture err(std::cerr);
RunScript("console.warn('careful'); console.error('broken');");
EXPECT_EQ(out.Str(), "");
EXPECT_EQ(log.Str(), "[WARN] careful\n");
EXPECT_EQ(err.Str(), "[ERROR] broken\n");
}
TEST_F(PolyfillsTest, ConsoleJoinsArgumentsWithSpaces) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
StreamCapture out(std::cout);
RunScript("console.log('a', 1, true, null, undefined, {}, [1, 2]);");
RunScript("console.log();");
EXPECT_EQ(out.Str(), "a 1 true null undefined [object Object] 1,2\n\n");
}
TEST_F(PolyfillsTest, FsExistsSync) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("present.txt", "x");
EXPECT_TRUE(ToBool(RunScript("_fs_polyfill.existsSync('" + file + "')")));
EXPECT_TRUE(ToBool(RunScript("_fs_polyfill.existsSync('" + dir.JsPath() + "')")));
EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync('" + dir.JsPath() + "/absent.txt')")));
// Anything that is not a path answers false rather than throwing.
EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync(42)")));
EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync()")));
}
TEST_F(PolyfillsTest, FsReadFileSyncDecodesWithAnEncoding) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("hello.txt", "Hello WebGPU\n");
// The encoding may be given on its own or inside an options object.
EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")),
"Hello WebGPU\n");
EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', {encoding: 'utf8'})")),
"Hello WebGPU\n");
// All buffer encodings are case-insensitive.
EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'UTF-8')")),
"Hello WebGPU\n");
EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', {encoding: 'Utf8'})")),
"Hello WebGPU\n");
}
TEST_F(PolyfillsTest, FsReadFileSyncReturnsBytesWithoutAnEncoding) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
// Bytes that are neither printable nor valid UTF-8, so a decoded read could not reproduce
// them.
std::string file = dir.WriteFile("bytes.bin", std::string("\x01\x02\x00\xff", 4));
EXPECT_TRUE(
ToBool(RunScript("_fs_polyfill.readFileSync('" + file + "') instanceof Uint8Array")));
EXPECT_EQ(
ToString(RunScript("Array.from(_fs_polyfill.readFileSync('" + file + "')).join(',')")),
"1,2,0,255");
}
TEST_F(PolyfillsTest, FsReadFileSyncThrowsForAMissingFile) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + dir.JsPath() + "/absent.txt')"));
}
TEST_F(PolyfillsTest, FsReadFileSyncPreservesNewlinesVerbatim) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("crlf.txt", "a\r\nb\n");
// The file is read as bytes whether or not it is decoded, so a CR survives. Opening in text
// mode on Windows would drop it and leave us disagreeing with Node.
EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")), "a\r\nb\n");
EXPECT_EQ(
ToString(RunScript("Array.from(_fs_polyfill.readFileSync('" + file + "')).join(',')")),
"97,13,10,98,10");
}
TEST_F(PolyfillsTest, FsReadFileSyncHandlesAnEmptyFile) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("empty.txt", "");
EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")), "");
EXPECT_EQ(ToUint32(RunScript("_fs_polyfill.readFileSync('" + file + "').length")), 0u);
}
// An option the polyfill cannot honour has to be reported, not ignored: quietly returning
// something that does not match what was asked for is worse than failing.
TEST_F(PolyfillsTest, FsRejectsUnsupportedOptions) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("hello.txt", "Hello WebGPU");
EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', {flag: 'r'})"));
EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', 'latin1')"));
EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', 42)"));
EXPECT_TRUE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', {withFileTypes: true})"));
EXPECT_TRUE(Throws("_fs_polyfill.statSync('" + file + "', {bigint: true})"));
// An absent, undefined or null options argument is not an unsupported one.
EXPECT_FALSE(Throws("_fs_polyfill.readFileSync('" + file + "')"));
EXPECT_FALSE(Throws("_fs_polyfill.readFileSync('" + file + "', undefined)"));
EXPECT_FALSE(Throws("_fs_polyfill.readFileSync('" + file + "', {encoding: null})"));
EXPECT_FALSE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', 'utf8')"));
}
// Node's readdirSync() answers with Buffers under the 'buffer' encoding, and its readFileSync()
// rejects that encoding outright. We reject it in both: there is no Buffer in this runtime, and
// handing back Uint8Arrays instead would break the first caller to call toString() on one.
TEST_F(PolyfillsTest, FsRejectsTheBufferEncoding) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("hello.txt", "Hello WebGPU");
EXPECT_TRUE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', 'buffer')"));
EXPECT_TRUE(Throws("_fs_polyfill.readdirSync('" + dir.JsPath() + "', {encoding: 'buffer'})"));
EXPECT_TRUE(Throws("_fs_polyfill.readFileSync('" + file + "', 'buffer')"));
// Bytes are still reachable from readFileSync, just by omitting the encoding rather than by
// naming one, which is what Node does too.
EXPECT_TRUE(
ToBool(RunScript("_fs_polyfill.readFileSync('" + file + "') instanceof Uint8Array")));
}
TEST_F(PolyfillsTest, FsRejectsUncPaths) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
// Node accepts UNC paths; this runtime deliberately refuses them, because Node and
// std::filesystem disagree about where a UNC root ends. String.raw keeps the backslashes away
// from JavaScript's escape rules so the path reaches the polyfill exactly as written.
EXPECT_TRUE(Throws(R"(_fs_polyfill.readFileSync(String.raw`\\server\share\a`))"));
EXPECT_TRUE(Throws(R"(_fs_polyfill.readdirSync(String.raw`\\server\share`))"));
EXPECT_TRUE(Throws(R"(_fs_polyfill.statSync(String.raw`\\server\share\a`))"));
// existsSync() answers false rather than throwing for arguments it cannot use, but a UNC path
// is not a question about whether something exists, so it is refused rather than reported
// absent.
EXPECT_TRUE(Throws(R"(_fs_polyfill.existsSync(String.raw`\\server\share\a`))"));
// The device forms share the prefix and go the same way.
EXPECT_TRUE(Throws(R"(_fs_polyfill.existsSync(String.raw`\\?\C:\a`))"));
EXPECT_TRUE(Throws(R"(_fs_polyfill.existsSync(String.raw`\\.\PhysicalDrive0`))"));
}
TEST_F(PolyfillsTest, FsAcceptsPathsThatMerelyResembleUnc) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("hello.txt", "Hello WebGPU");
// It takes two leading separators to make a UNC path. One is not enough, and a backslash
// anywhere else is just a character. None of these exist, but none of them are refused
// either - the distinction being that a refusal throws.
EXPECT_FALSE(ToBool(RunScript(R"(_fs_polyfill.existsSync(String.raw`\server\share`))")));
EXPECT_FALSE(ToBool(RunScript(R"(_fs_polyfill.existsSync('a\\b'))")));
// Where the two platforms part company. On Windows "//server/share" is UNC and refused; on
// POSIX a leading "//" is a legal path that the path module already handles correctly, so it
// is left alone.
#if defined(_WIN32)
EXPECT_TRUE(Throws("_fs_polyfill.existsSync('//no/such/path')"));
#else
EXPECT_FALSE(ToBool(RunScript("_fs_polyfill.existsSync('//no/such/path')")));
#endif
// And an ordinary path is untouched by any of this.
EXPECT_EQ(ToString(RunScript("_fs_polyfill.readFileSync('" + file + "', 'utf8')")),
"Hello WebGPU");
}
TEST_F(PolyfillsTest, FsReaddirSync) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
dir.WriteFile("a.txt", "a");
dir.WriteFile("b.txt", "b");
// The entries are names, not paths, and the order is the filesystem's.
EXPECT_EQ(
ToString(RunScript("_fs_polyfill.readdirSync('" + dir.JsPath() + "').sort().join(',')")),
"a.txt,b.txt");
}
TEST_F(PolyfillsTest, FsStatSync) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("file.txt", "x");
EXPECT_TRUE(ToBool(RunScript("const f = _fs_polyfill.statSync('" + file +
"'); f.isFile() && !f.isDirectory()")));
EXPECT_TRUE(ToBool(RunScript("const d = _fs_polyfill.statSync('" + dir.JsPath() +
"'); d.isDirectory() && !d.isFile()")));
}
TEST_F(PolyfillsTest, FsReadFileCallbackRunsAsynchronously) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("hello.txt", "Hello WebGPU");
RunScript(
"globalThis.error = 'unset'; globalThis.data = 'unset';"
"_fs_polyfill.readFile('" +
file + "', 'utf8', (err, data) => { globalThis.error = err; globalThis.data = data; });");
// Nothing has happened yet: that the callback waits for the microtask checkpoint is what
// makes this the asynchronous form.
EXPECT_EQ(ToString(RunScript("globalThis.data")), "unset");
RunMicrotasks();
EXPECT_TRUE(ToBool(RunScript("globalThis.error === null")));
EXPECT_EQ(ToString(RunScript("globalThis.data")), "Hello WebGPU");
}
TEST_F(PolyfillsTest, FsReadFileCallbackReportsFailure) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
// With the encoding omitted the callback takes the second argument position.
RunScript(
"globalThis.error = 'unset';"
"_fs_polyfill.readFile('" +
dir.JsPath() + "/absent.txt', (err) => { globalThis.error = err; });");
RunMicrotasks();
EXPECT_TRUE(ToBool(RunScript("globalThis.error instanceof Error")));
}
TEST_F(PolyfillsTest, FsPromisesResolve) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string file = dir.WriteFile("hello.txt", "Hello WebGPU");
RunScript(
"globalThis.data = 'unset';"
"_fs_polyfill.promises.readFile('" +
file + "', 'utf8').then((d) => { globalThis.data = d; });");
EXPECT_EQ(ToString(RunScript("globalThis.data")), "unset");
RunMicrotasks();
EXPECT_EQ(ToString(RunScript("globalThis.data")), "Hello WebGPU");
RunScript(
"globalThis.entries = 'unset';"
"_fs_polyfill.promises.readdir('" +
dir.JsPath() + "').then((e) => { globalThis.entries = e.join(','); });");
RunMicrotasks();
EXPECT_EQ(ToString(RunScript("globalThis.entries")), "hello.txt");
RunScript(
"globalThis.isFile = 'unset';"
"_fs_polyfill.promises.stat('" +
file + "').then((s) => { globalThis.isFile = s.isFile(); });");
RunMicrotasks();
EXPECT_TRUE(ToBool(RunScript("globalThis.isFile")));
}
TEST_F(PolyfillsTest, FsPromisesRejectWhenTheSyncCallThrows) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
RunScript(
"globalThis.reason = 'unset';"
"_fs_polyfill.promises.stat('" +
dir.JsPath() + "/absent').catch((e) => { globalThis.reason = e; });");
RunMicrotasks();
EXPECT_TRUE(ToBool(RunScript("globalThis.reason instanceof Error")));
}
// The path module answers in the host's separator, so the expectations below are written in their
// POSIX spelling and translated. dirname() is the exception: Node slices its answer out of the
// input, so whatever separator was written comes back unchanged.
std::string Native(std::string_view posix) {
std::string native(posix);
#if defined(_WIN32)
std::replace(native.begin(), native.end(), '/', '\\');
#endif
return native;
}
// A rooted path that names no drive is resolved against the drive of the working directory, as it
// is in Node, so an absolute expectation has to carry that drive on Windows.
std::string RootedNative(std::string_view posix) {
return std::filesystem::current_path().root_name().string() + Native(posix);
}
TEST_F(PolyfillsTest, PathNormalize) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('/a/b/../c/./d')")), Native("/a/c/d"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a//b')")), Native("a/b"));
// A trailing slash says the path names a directory, so it is kept.
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a//b/')")), Native("a/b/"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('/a/b/')")), Native("/a/b/"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('./')")), Native("./"));
// The input is what decides that, and only the input: lexically_normal() invents a trailing
// separator of its own whenever the last component was a dot segment, and it has to be
// discarded. Without that, the first of these answers 'a/'.
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a/b/..')")), Native("a"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('a/b/../')")), Native("a/"));
// A '..' that cannot be cancelled is kept in a relative path but dropped at the root.
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('../../a')")), Native("../../a"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('/..')")), Native("/"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('')")), ".");
#if !defined(_WIN32)
// A leading run of separators collapses to one. std::filesystem keeps it, because POSIX
// reserves "//" for the implementation. POSIX-only: on Windows this shape is UNC.
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('//a/b')")), "/a/b");
#endif
}
TEST_F(PolyfillsTest, PathJoin) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', 'b', 'c')")), Native("a/b/c"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('/a', 'b/', '../c')")), Native("/a/c"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', '', 'b')")), Native("a/b"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('')")), ".");
// join() concatenates unconditionally, where operator/ would let a rooted argument discard
// everything before it and answer '/b'.
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('/a', '/b')")), Native("/a/b"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('/', 'a')")), Native("/a"));
// join() normalizes, so the trailing slash of the last argument survives, and a '..' argument
// does not leave one behind.
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', 'b/')")), Native("a/b/"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('a', 'b', '..')")), Native("a"));
}
TEST_F(PolyfillsTest, PathDirname) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
// The answer is a slice of the argument, so it comes back in the separator it was written in
// on either platform.
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a/b/c.js')")), "/a/b");
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a')")), "/");
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('a.js')")), ".");
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('')")), ".");
// Unlike normalize(), dirname() ignores a trailing slash rather than reading it as a segment
// boundary, so these name the parent of b, not b itself.
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a/b/')")), "/a");
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('a/')")), ".");
// And it does not normalize what it keeps, which is why it cannot be parent_path(): that
// would answer '/a'.
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('/a//b')")), "/a/");
#if !defined(_WIN32)
// Nothing but separators names the root, and a leading "//" is kept whole. POSIX-only: on
// Windows two leading separators are UNC, which is refused rather than answered.
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('///')")), "/");
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('//a')")), "//");
#endif
}
TEST_F(PolyfillsTest, PathResolve) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b', 'c')")), RootedNative("/a/b/c"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b', '../c')")), RootedNative("/a/c"));
// An absolute argument discards everything to its left, and a call with no absolute argument
// at all is taken relative to the working directory.
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b', '/c')")), RootedNative("/c"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('a')")),
(std::filesystem::current_path() / "a").string());
// Unlike normalize(), resolve() never answers with a trailing separator, whether the argument
// carried one or lexically_normal() invented one.
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b/')")), RootedNative("/a/b"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a/b/..')")), RootedNative("/a"));
// An empty argument is skipped rather than read as '.', and a call with nothing left is the
// working directory. std::filesystem::absolute('') fails outright.
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('/a', '')")), RootedNative("/a"));
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('')")),
std::filesystem::current_path().string());
}
TEST_F(PolyfillsTest, PathRelative) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('/a/b', '/a/b/c/d')")), Native("c/d"));
// Identical paths are "" and not ".", which is what lexically_relative() answers.
EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('/a/b', '/a/b')")), "");
// Leaving the common prefix behind takes one '..' per remaining segment of `from`.
EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('/a/b/c', '/a/d')")), Native("../../d"));
// A zero-length side means the working directory, because resolve() is what defines both ends.
EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('', 'a')")), "a");
}
TEST_F(PolyfillsTest, PathRejectsUncPaths) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
// The same refusal the fs entry points make, for the same reason: Node and std::filesystem
// disagree about where a UNC root ends. Any argument can carry the prefix, not just the first.
EXPECT_TRUE(Throws(R"(_path_polyfill.normalize(String.raw`\\server\share\a`))"));
EXPECT_TRUE(Throws(R"(_path_polyfill.dirname(String.raw`\\server\share\a`))"));
EXPECT_TRUE(Throws(R"(_path_polyfill.join('a', String.raw`\\server\share`))"));
EXPECT_TRUE(Throws(R"(_path_polyfill.resolve(String.raw`\\server\share`))"));
EXPECT_TRUE(Throws(R"(_path_polyfill.relative('a', String.raw`\\?\C:\a`))"));
}
#if defined(_WIN32)
// The shapes that have no POSIX spelling to translate. Every expectation here is what
// path.win32 answers.
TEST_F(PolyfillsTest, PathDriveLetters) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
// A drive with no root directory names the working directory on that drive, and Node spells
// the implied here-ness out rather than leaving the bare drive.
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:')")), "C:.");
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:.')")), "C:.");
// Either separator is accepted on the way in; the preferred one comes back out.
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:/a/b/')")), "C:\\a\\b\\");
EXPECT_EQ(ToString(RunScript("_path_polyfill.normalize('C:/a/b/..')")), "C:\\a");
EXPECT_EQ(ToString(RunScript("_path_polyfill.join('C:/a', 'b/')")), "C:\\a\\b\\");
EXPECT_EQ(ToString(RunScript("_path_polyfill.resolve('C:/a', 'b')")), "C:\\a\\b");
EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('C:/a/b', 'C:/a/c')")), "..\\c");
// Except from dirname(), which slices the argument and so keeps what was written. The drive
// is the root here, and is never cut into.
EXPECT_EQ(ToString(RunScript(R"(_path_polyfill.dirname(String.raw`C:\a\b`))")), "C:\\a");
EXPECT_EQ(ToString(RunScript("_path_polyfill.dirname('C:/a//b')")), "C:/a/");
EXPECT_EQ(ToString(RunScript(R"(_path_polyfill.dirname(String.raw`C:\a`))")), "C:\\");
// Two drives are unrelated, so there is no path from one to the other and the answer is just
// the destination. lexically_relative() says "" for that, which would read as "the same
// place". Purely lexical, so the drives need not exist.
EXPECT_EQ(ToString(RunScript("_path_polyfill.relative('C:/a', 'D:/b')")), "D:\\b");
}
#endif
TEST_F(PolyfillsTest, PathRejectsArgumentsThatAreNotStrings) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(Throws("_path_polyfill.normalize()"));
EXPECT_TRUE(Throws("_path_polyfill.normalize(5)"));
EXPECT_TRUE(Throws("_path_polyfill.join('a', null)"));
EXPECT_TRUE(Throws("_path_polyfill.relative('a')"));
}
TEST_F(PolyfillsTest, PathSep) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("_path_polyfill.sep")), Native("/"));
}
TEST_F(PolyfillsTest, ProcessArgvAndCwd) {
dawn::node::standalone::PolyfillOptions options;
options.argv = {"runner", "arg1", "arg2"};
dawn::node::standalone::RegisterPolyfills(env_, loop(), options);
EXPECT_EQ(ToUint32(RunScript("process.argv.length")), 3u);
EXPECT_EQ(ToString(RunScript("process.argv.join(',')")), "runner,arg1,arg2");
// cwd() must report the real working directory, not merely something of type string.
EXPECT_EQ(ToString(RunScript("process.cwd()")), std::filesystem::current_path().string());
}
TEST_F(PolyfillsTest, ProcessArgvIsEmptyByDefault) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(ToBool(RunScript("Array.isArray(process.argv)")));
EXPECT_EQ(ToUint32(RunScript("process.argv.length")), 0u);
}
TEST_F(PolyfillsTest, ProcessEnvExposesDawnFlags) {
dawn::ScopedEnvironmentVar dawn_flags("DAWN_FLAGS", "--a-flag");
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("process.env.DAWN_FLAGS")), "--a-flag");
}
TEST_F(PolyfillsTest, ProcessEnvIsEmptyWithoutDawnFlags) {
dawn::ScopedEnvironmentVar dawn_flags("DAWN_FLAGS", nullptr);
dawn::node::standalone::RegisterPolyfills(env_, loop());
// `process.env` is not the real environment: only the variables the runner reads are copied
// into it, so with DAWN_FLAGS unset it is empty.
EXPECT_EQ(ToUint32(RunScript("Object.keys(process.env).length")), 0u);
}
TEST_F(PolyfillsTest, ProcessExitStopsTheLoop) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScriptAllowingExit("process.exit(42)");
EXPECT_TRUE(loop().stopped());
EXPECT_EQ(loop().exit_code(), 42);
}
TEST_F(PolyfillsTest, ProcessExitDefaultsToZero) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScriptAllowingExit("process.exit()");
EXPECT_TRUE(loop().stopped());
EXPECT_EQ(loop().exit_code(), 0);
}
TEST_F(PolyfillsTest, ProcessExitWithACodeThatIsNotANumberExitsWithZero) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScriptAllowingExit("process.exit('not a number')");
EXPECT_TRUE(loop().stopped());
EXPECT_EQ(loop().exit_code(), 0);
}
TEST_F(PolyfillsTest, ProcessStreamWritesAreVerbatim) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
StreamCapture out(std::cout);
StreamCapture log(std::clog);
RunScript("process.stdout.write('no'); process.stdout.write(' newline');");
RunScript("process.stderr.write('unlabelled');");
// Unlike console.log(), write() adds neither a newline nor a prefix.
EXPECT_EQ(out.Str(), "no newline");
EXPECT_EQ(log.Str(), "unlabelled");
}
TEST_F(PolyfillsTest, ProcessStreamWriteReturnsTrue) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
StreamCapture out(std::cout);
StreamCapture log(std::clog);
// write() reports whether the caller may write again immediately, rather than waiting for a
// 'drain' event.
EXPECT_TRUE(ToBool(RunScript("process.stdout.write('x')")));
EXPECT_TRUE(ToBool(RunScript("process.stderr.write('x')")));
}
// process.exit() must not return to JavaScript: once it is called, no further JavaScript runs,
// JavaScript cannot catch the unwind, and a later exit() cannot overwrite the exit code.
TEST_F(PolyfillsTest, ProcessExitDoesNotReturnToJavaScript) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript("globalThis._log = []; globalThis._record = (s) => { globalThis._log.push(s); };");
RunScriptAllowingExit(R"(
try {
process.exit(3);
_record('after-exit');
} catch (e) {
_record('caught');
} finally {
_record('finally');
}
_record('after-try');
process.exit(0);
)");
// The second exit() was never reached, so the first exit code stands, and nothing after the
// exit() ran - not even the `finally` block, which JavaScript would otherwise always run.
EXPECT_TRUE(loop().stopped());
EXPECT_EQ(loop().exit_code(), 3);
EXPECT_EQ(ToString(RunScript("globalThis._log.join(',')")), "");
}
TEST_F(PolyfillsTest, ProcessExitFromMicrotaskDoesNotReturnToJavaScript) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._log = [];
globalThis._record = (s) => { globalThis._log.push(s); };
Promise.resolve().then(() => {
process.exit(3);
_record('after-exit');
});
Promise.resolve().then(() => { _record('later-microtask'); });
)");
RunMicrotasks();
isolate_->CancelTerminateExecution();
// Neither the rest of the microtask that exited nor the microtask queued behind it ran.
EXPECT_TRUE(loop().stopped());
EXPECT_EQ(loop().exit_code(), 3);
EXPECT_EQ(ToString(RunScript("globalThis._log.join(',')")), "");
}
TEST_F(PolyfillsTest, EventGlobals) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(
ToBool(RunScript("typeof EventTarget === 'function' && "
"typeof Event === 'function' && "
"typeof CustomEvent === 'function' && "
"typeof MessageEvent === 'function' && "
"typeof DOMException === 'function'")));
// EventTarget dispatches to both function listeners and { handleEvent } object listeners.
EXPECT_EQ(ToString(RunScript(R"(
const target = new EventTarget();
const log = [];
target.addEventListener('test', function(e) {
log.push('fn:' + (this === target) + ':' + e.type);
});
target.addEventListener('test', {
tag: 'obj',
handleEvent(e) {
log.push(this.tag + ':' + e.type);
},
});
target.dispatchEvent(new Event('test'));
log.join(',')
)")),
"fn:true:test,obj:test");
}
TEST_F(PolyfillsTest, DOMException) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript(R"(
const named = new DOMException('bad state', 'InvalidStateError');
const unnamed = new DOMException('failed');
[
named instanceof DOMException,
named instanceof Error,
named.name,
named.message,
unnamed.name,
].join(',')
)")),
"true,true,InvalidStateError,bad state,Error");
}
TEST_F(PolyfillsTest, EventCancellation) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._target = new EventTarget();
globalThis._target.addEventListener('err', (e) => { e.preventDefault(); });
globalThis._target.addEventListener('ok', () => {});
)");
// A cancelable event is cancelled when preventDefault() is called.
napi_value res = RunScript(R"(
const cancelable = new Event('err', { cancelable: true });
[globalThis._target.dispatchEvent(cancelable), cancelable.defaultPrevented].join(',')
)");
EXPECT_EQ(ToString(res), "false,true");
// An event that is not cancelable ignores preventDefault().
res = RunScript(R"(
const plain = new Event('err');
[globalThis._target.dispatchEvent(plain), plain.defaultPrevented].join(',')
)");
EXPECT_EQ(ToString(res), "true,false");
// A cancelable event where preventDefault() is not called remains uncancelled.
res = RunScript(R"(
const uncancelled = new Event('ok', { cancelable: true });
[globalThis._target.dispatchEvent(uncancelled), uncancelled.defaultPrevented].join(',')
)");
EXPECT_EQ(ToString(res), "true,false");
}
TEST_F(PolyfillsTest, EventListenerOnce) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
napi_value res = RunScript(R"(
const target = new EventTarget();
let once = 0;
let always = 0;
target.addEventListener('x', () => { once++; }, { once: true });
target.addEventListener('x', () => { always++; });
target.dispatchEvent(new Event('x'));
target.dispatchEvent(new Event('x'));
[once, always].join(',')
)");
EXPECT_EQ(ToString(res), "1,2");
}
TEST_F(PolyfillsTest, EventTargetDeduplicatesByTypeAndCapture) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
napi_value res = RunScript(R"(
const target = new EventTarget();
const log = [];
const fn = () => { log.push('fn'); };
const other = () => { log.push('other'); };
target.addEventListener('x', fn);
target.addEventListener('x', other);
target.addEventListener('x', fn);
target.addEventListener('x', fn, false);
target.addEventListener('x', fn, { capture: false });
target.addEventListener('x', fn, true);
target.addEventListener('x', fn, { capture: true });
target.dispatchEvent(new Event('x'));
const afterBoth = log.join(',');
log.length = 0;
target.removeEventListener('x', fn, false);
target.dispatchEvent(new Event('x'));
const afterRemoveBubble = log.join(',');
log.length = 0;
target.removeEventListener('x', fn, { capture: true });
target.dispatchEvent(new Event('x'));
const afterRemoveCapture = log.join(',');
[afterBoth, afterRemoveBubble, afterRemoveCapture].join(' | ')
)");
EXPECT_EQ(ToString(res), "fn,other,fn | other,fn | other");
}
TEST_F(PolyfillsTest, CustomEvent) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
napi_value res = RunScript(R"(
const target = new EventTarget();
let seen = null;
target.addEventListener('bar', (e) => { seen = e; }, { once: true });
target.dispatchEvent(new CustomEvent('bar'));
[seen instanceof CustomEvent, seen instanceof Event, seen.type].join(',')
)");
EXPECT_EQ(ToString(res), "true,true,bar");
}
TEST_F(PolyfillsTest, MessageEvent) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
napi_value res = RunScript(R"(
const withData = new MessageEvent('import', { data: { url: 'a.spec.js' } });
const withoutData = new MessageEvent('finish');
[
withData instanceof MessageEvent,
withData instanceof Event,
withData.type,
withData.data.url,
withoutData.data === undefined,
].join(',')
)");
EXPECT_EQ(ToString(res), "true,true,import,a.spec.js,true");
}
TEST_F(PolyfillsTest, ProcessVersions) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
napi_value res = RunScript(
"String(typeof process.versions === 'object' && "
"typeof process.versions.node === 'string' && process.versions.node.length > 0)");
EXPECT_EQ(ToString(res), "true");
}
TEST_F(PolyfillsTest, TextEncoder) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
// Non-ASCII input covers all of the UTF-8 sequence lengths: two bytes for e-acute, three for
// the arrow and four for the surrogate pair.
napi_value res = RunScript(R"(
const bytes = new TextEncoder().encode('h\u00e9llo \u2192 \ud83c\udf0d');
Array.from(bytes).join(' ')
)");
EXPECT_EQ(ToString(res), "104 195 169 108 108 111 32 226 134 146 32 240 159 140 141");
// Undefined input encodes to an empty Uint8Array, and an unpaired surrogate is replaced with
// U+FFFD (EF BF BD) rather than throwing.
res = RunScript(R"(
[new TextEncoder().encode().length,
Array.from(new TextEncoder().encode('\ud800')).join(' ')].join(',')
)");
EXPECT_EQ(ToString(res), "0,239 191 189");
}
TEST_F(PolyfillsTest, RequireBuiltins) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
napi_value res = RunScript(R"(
globalThis._webgpu_module = { marker: 'wgpu' };
[
require('fs') === globalThis._fs_polyfill,
require('node:fs') === globalThis._fs_polyfill,
require('path') === globalThis._path_polyfill,
require('node:path') === globalThis._path_polyfill,
require('process') === globalThis.process,
require('node:process') === globalThis.process,
require('perf_hooks').performance === globalThis.performance,
require('node:perf_hooks').performance === globalThis.performance,
require('./dawn.node').marker,
require('/any/dir/dawn.node').marker,
].join(',')
)");
EXPECT_EQ(ToString(res), "true,true,true,true,true,true,true,true,wgpu,wgpu");
}
TEST_F(PolyfillsTest, RequireModules) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
// `sub/leaf.cjs` records the `__dirname` and `__filename` arguments passed to its module
// wrapper function so we can verify relative resolution from `sub/main.js`. Backslashes are
// normalized to '/' so the suffix check holds on Windows too.
dir.WriteFile("sub/leaf.cjs", R"(
exports.dir = __dirname.replace(/\\/g, '/');
exports.file = __filename.replace(/\\/g, '/');
)");
// `sub/main.js` increments a global counter each time its body executes and re-exports `leaf`.
std::string main_mod = dir.WriteFile("sub/main.js", R"(
globalThis.loadCount = (globalThis.loadCount || 0) + 1;
module.exports = require('./leaf.cjs');
)");
// Require `sub/main.js` twice: the first call executes `main.js` and `leaf.cjs`, and the second
// call must return the cached `exports` object without executing `main.js` a second time.
RunScript("globalThis.first = require('" + main_mod + "'); globalThis.second = require('" +
main_mod + "');");
EXPECT_TRUE(ToBool(RunScript("globalThis.first.dir.endsWith('/sub')")));
EXPECT_TRUE(ToBool(RunScript("globalThis.first.file.endsWith('/sub/leaf.cjs')")));
EXPECT_TRUE(ToBool(RunScript("globalThis.first === globalThis.second")));
EXPECT_EQ(ToUint32(RunScript("globalThis.loadCount")), 1u);
}
// `LoadModule` inserts the module into `module_cache` before running its body so a circular
// `require()` receives the in-progress `exports` object instead of recursing infinitely.
TEST_F(PolyfillsTest, RequireCircularDependency) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
// 1. `cycle_a.js` sets `exports.stage = 'a-init'`, then pauses to require `cycle_b.js`.
// 2. `cycle_b.js` requires `cycle_a.js` while `cycle_a.js` is still running, so `cycle_b.js`
// observes the in-progress `a.stage` value ('a-init').
// 3. `cycle_a.js` resumes and overwrites `exports.stage = 'a-done'`.
std::string cycle_a = dir.WriteFile("cycle_a.js", R"(
exports.stage = 'a-init';
const b = require('./cycle_b.js');
exports.seenByB = b.fromA;
exports.stage = 'a-done';
)");
dir.WriteFile("cycle_b.js", R"(
const a = require('./cycle_a.js');
exports.fromA = a.stage;
)");
RunScript("globalThis.cycle = require('" + cycle_a + "');");
EXPECT_EQ(ToString(RunScript("globalThis.cycle.seenByB")), "a-init");
EXPECT_EQ(ToString(RunScript("globalThis.cycle.stage")), "a-done");
}
TEST_F(PolyfillsTest, RequireRejectsInvalidAndBareSpecifiers) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
dir.WriteFile("hello/t.txt", "module.exports = 'should not load';");
EXPECT_TRUE(Throws("require()"));
EXPECT_TRUE(Throws("require(42)"));
EXPECT_TRUE(Throws("require('ansi-colors')"));
EXPECT_TRUE(Throws("require('hello/t.txt')"));
EXPECT_TRUE(Throws(R"(require(String.raw`\\server\share\a.js`))"));
EXPECT_TRUE(Throws("require('" + dir.JsPath() + "/absent.js')"));
}
// `LoadModule` caches a module's `exports` object before compiling and running its body (to
// support circular dependencies). If compilation or execution fails, that entry must be erased from
// `module_cache`; otherwise a second `require()` of the same path would hit the cache and return
// the half-initialized `exports` object without throwing.
TEST_F(PolyfillsTest, RequireDoesNotCacheFailedModules) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string bad_syntax = dir.WriteFile("bad_syntax.js", "const = ;");
std::string runtime_throw = dir.WriteFile("runtime_throw.js", R"(
exports.partial = true;
throw new Error('init failed');
)");
// First call fails to compile; second call verifies the failed module was not left in the
// cache.
EXPECT_TRUE(Throws("require('" + bad_syntax + "')"));
EXPECT_TRUE(Throws("require('" + bad_syntax + "')"));
// First call throws mid-execution after mutating `exports.partial`; second call verifies the
// partial `exports` object was evicted from the cache and throws again.
EXPECT_TRUE(Throws("require('" + runtime_throw + "')"));
EXPECT_TRUE(Throws("require('" + runtime_throw + "')"));
}
// require() compiles modules with v8::ScriptCompiler::CompileFunction() so stack traces preserve
// the source file's 1-based line and column numbers.
TEST_F(PolyfillsTest, RequireStackTraceLineAndColumn) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
TempDir dir;
std::string thrower = dir.WriteFile("thrower.js", "\n\n throw new Error('boom');");
std::string first = dir.WriteFile("first.js", "throw new Error('boom');");
napi_value res = RunScript(
"let thrown = 'no throw';"
"try { require('" +
thrower +
"'); } catch (e) { thrown = e.stack.match(/thrower\\.js:[0-9]+:[0-9]+/)[0]; }"
"thrown");
EXPECT_EQ(ToString(res), "thrower.js:3:9");
res = RunScript(
"let firstThrown = 'no throw';"
"try { require('" +
first +
"'); } catch (e) { firstThrown = e.stack.match(/first\\.js:[0-9]+:[0-9]+/)[0]; }"
"firstThrown");
EXPECT_EQ(ToString(res), "first.js:1:7");
}
TEST_F(PolyfillsTest, PerformanceNow) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("typeof performance.now()")), "number");
double now = ToDouble(RunScript("performance.now()"));
EXPECT_GE(now, 0.0);
// now() counts from registration, so a test that has only just started cannot be minutes in.
// A clock returning, say, milliseconds since the epoch would fail here.
EXPECT_LT(now, 60000.0);
}
// These only establish that the clock never runs backwards. Sampling in a loop cannot show that
// it runs forwards at all, because performance.now() reports whole and fractional milliseconds
// and a thousand calls can finish inside one of them; PerformanceNowAdvances covers that.
TEST_F(PolyfillsTest, PerformanceNowIsMonotonic) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(ToBool(RunScript(R"((function() {
let last = performance.now();
for (let i = 0; i < 1000; ++i) {
const next = performance.now();
if (next < last) {
return false;
}
last = next;
}
return true;
})())")));
}
TEST_F(PolyfillsTest, PerformanceNowAdvances) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
double before = ToDouble(RunScript("performance.now()"));
std::this_thread::sleep_for(kSleepDuration);
double elapsed_ms = ToDouble(RunScript("performance.now()")) - before;
// sleep_for() blocks for at least the duration asked of it, so the clock really has moved.
// Bounding the reading from above as well as below also pins the unit: a clock reporting
// seconds would read 0.05 here, and one reporting microseconds 50000.
EXPECT_GE(elapsed_ms, kMinElapsedMs);
EXPECT_LT(elapsed_ms, kMaxElapsedMs);
}
TEST_F(PolyfillsTest, HrtimeBigintIsMonotonic) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_EQ(ToString(RunScript("typeof process.hrtime.bigint()")), "bigint");
EXPECT_TRUE(ToBool(RunScript(R"((function() {
let last = process.hrtime.bigint();
for (let i = 0; i < 1000; ++i) {
const next = process.hrtime.bigint();
if (next < last) {
return false;
}
last = next;
}
return true;
})())")));
}
TEST_F(PolyfillsTest, HrtimeBigintAdvances) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript("globalThis.hrtimeStart = process.hrtime.bigint();");
std::this_thread::sleep_for(kSleepDuration);
// The difference is taken in BigInt and only then converted, so no precision is lost before
// the division. As above, the upper bound is what pins the unit to nanoseconds.
double elapsed_ms =
ToDouble(RunScript("Number(process.hrtime.bigint() - globalThis.hrtimeStart) / 1e6"));
EXPECT_GE(elapsed_ms, kMinElapsedMs);
EXPECT_LT(elapsed_ms, kMaxElapsedMs);
}
TEST_F(PolyfillsTest, SetImmediateQueuesTheCallbackOnTheLoop) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._immediateRan = false;
setImmediate(() => {
globalThis._immediateRan = true;
});
)");
EXPECT_FALSE(ToBool(RunScript("globalThis._immediateRan")));
loop().RunOneIteration();
EXPECT_TRUE(ToBool(RunScript("globalThis._immediateRan")));
}
// Arguments after the callback are forwarded to it, as Node and the web platform do.
TEST_F(PolyfillsTest, SetImmediateForwardsExtraArgumentsToTheCallback) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._immediateArgs = null;
setImmediate((a, b) => {
globalThis._immediateArgs = [a, b];
}, 'x', 42);
)");
loop().RunOneIteration();
EXPECT_EQ(ToString(RunScript("globalThis._immediateArgs.join(',')")), "x,42");
}
TEST_F(PolyfillsTest, SetTimeoutQueuesTheCallbackAndClearTimeoutWithdrawsIt) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._clearedRan = false;
globalThis._timeoutRan = false;
const handle = setTimeout(() => {
globalThis._clearedRan = true;
}, 0);
clearTimeout(handle);
setTimeout(() => {
globalThis._timeoutRan = true;
}, 0);
)");
loop().RunOneIteration();
EXPECT_FALSE(ToBool(RunScript("globalThis._clearedRan")));
EXPECT_TRUE(ToBool(RunScript("globalThis._timeoutRan")));
}
TEST_F(PolyfillsTest, QueueMicrotask) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._microtaskRan = false;
queueMicrotask(() => {
globalThis._microtaskRan = true;
});
)");
EXPECT_FALSE(ToBool(RunScript("globalThis._microtaskRan")));
RunMicrotasks();
EXPECT_TRUE(ToBool(RunScript("globalThis._microtaskRan")));
}
TEST_F(PolyfillsTest, TimerGlobals) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(
ToBool(RunScript("typeof setImmediate === 'function' && "
"typeof setTimeout === 'function' && "
"typeof clearTimeout === 'function' && "
"typeof queueMicrotask === 'function'")));
}
// Arguments after the delay are forwarded to the callback, as Node and the web platform do.
TEST_F(PolyfillsTest, SetTimeoutForwardsExtraArgumentsToTheCallback) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._timeoutArgs = null;
setTimeout((a, b) => {
globalThis._timeoutArgs = [a, b];
}, 0, 'x', 42);
)");
loop().RunOneIteration();
EXPECT_EQ(ToString(RunScript("globalThis._timeoutArgs.join(',')")), "x,42");
}
// Negative, NaN, non-numeric and missing delays are all treated as zero, matching the web
// platform's clamping rules, so all four of these come due in the very first iteration.
TEST_F(PolyfillsTest, ADelayThatIsNotAPositiveNumberIsTreatedAsZero) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
RunScript(R"(
globalThis._runs = 0;
const count = () => { globalThis._runs++; };
setTimeout(count, -50);
setTimeout(count, NaN);
setTimeout(count, 'not a number');
setTimeout(count);
)");
loop().RunOneIteration();
EXPECT_DOUBLE_EQ(ToDouble(RunScript("globalThis._runs")), 4.0);
}
// The delay reaches the loop as a duration rather than being dropped or rounded to zero. The loop
// reads the clock inside setTimeout(), between `before` and `after`, so `due` is bounded on both
// sides by `before + 10s <= due <= after + 10s` regardless of how long RunScript() takes.
TEST_F(PolyfillsTest, TheDelayIsPassedThroughToTheLoop) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
const auto before = dawn::node::standalone::EventLoop::Clock::now();
RunScript("setTimeout(() => {}, 10000)");
const auto after = dawn::node::standalone::EventLoop::Clock::now();
const std::optional<dawn::node::standalone::EventLoop::TimePoint> due = loop().NextDueTime();
ASSERT_TRUE(due.has_value());
EXPECT_GE(*due, before + std::chrono::seconds(10));
EXPECT_LE(*due, after + std::chrono::seconds(10));
}
TEST_F(PolyfillsTest, TimersRejectCallbacksThatAreNotFunctions) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(Throws("setImmediate()"));
EXPECT_TRUE(Throws("setImmediate(42)"));
EXPECT_TRUE(Throws("setTimeout()"));
EXPECT_TRUE(Throws("setTimeout('globalThis.x = 1', 10)"));
}
// Clearing a handle that was never issued, or one that has already fired, is defined to do nothing
// rather than to fail.
TEST_F(PolyfillsTest, ClearingAnUnknownTimerIsNotAnError) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_FALSE(Throws("clearTimeout()"));
EXPECT_FALSE(Throws("clearTimeout(undefined)"));
EXPECT_FALSE(Throws("clearTimeout('not a handle')"));
EXPECT_FALSE(Throws("clearTimeout(9999)"));
// Clearing twice, and clearing after the timer has already fired, are both no-ops.
RunScript(R"(
globalThis._ran = false;
globalThis._handle = setTimeout(() => {
globalThis._ran = true;
}, 0);
)");
loop().RunOneIteration();
EXPECT_TRUE(ToBool(RunScript("globalThis._ran")));
EXPECT_FALSE(Throws("clearTimeout(globalThis._handle)"));
EXPECT_FALSE(Throws("clearTimeout(globalThis._handle)"));
}
// Handles have to be distinct, or clearing one timer would cancel another.
TEST_F(PolyfillsTest, TimerHandlesAreUnique) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(ToBool(RunScript(R"(
const noop = () => {};
const handles = [setTimeout(noop, 1), setTimeout(noop, 1), setTimeout(noop, 1)];
new Set(handles).size === handles.length;
)")));
}
// Node hands back a Timeout object here. The CTS only ever passes the value back to clearTimeout(),
// so a number is enough.
TEST_F(PolyfillsTest, SetTimeoutReturnsANumericHandle) {
dawn::node::standalone::RegisterPolyfills(env_, loop());
EXPECT_TRUE(ToBool(RunScript("typeof setTimeout(() => {}, 0) === 'number'")));
}
} // namespace
#pragma clang diagnostic pop