blob: 43b5ec29f363b0d94c0d39da5f4f1415a4675bce [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 <chrono>
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
#include "src/dawn/node/standalone/EventLoop.h"
#include "src/dawn/node/test/V8TestEnvironment.h"
namespace dawn::node::standalone {
namespace {
using IterationResult = EventLoop::IterationResult;
using std::chrono::milliseconds;
// An EventLoop whose clock the test moves by hand, so that delays cost no wall time and the order
// tasks run in is fully determined rather than merely likely. Run() reads the real clock when it
// sleeps and so cannot be used with this; the tests that cover Run() use a plain EventLoop.
class TestEventLoop : public EventLoop {
public:
using EventLoop::EventLoop;
// Moves the clock forward without running anything.
void AdvanceBy(Duration delta) { now_ += delta; }
protected:
TimePoint Now() const override { return now_; }
private:
// Start away from the epoch so that a time computed from a negative delay is still orderable.
TimePoint now_ = TimePoint() + std::chrono::hours(1);
};
// Redirects std::cerr into a buffer for its lifetime, so that a test that expects the loop to
// report an uncaught exception can assert on the report rather than printing it.
class StderrCapture {
public:
StderrCapture() : original_(std::cerr.rdbuf(buffer_.rdbuf())) {}
~StderrCapture() { std::cerr.rdbuf(original_); }
std::string str() const { return buffer_.str(); }
private:
std::ostringstream buffer_;
std::streambuf* const original_;
};
// Renders the log of what ran as one string, so that a failure prints the whole order rather than
// the first element that differs.
std::string Join(const std::vector<std::string>& entries) {
std::string joined;
for (const std::string& entry : entries) {
if (!joined.empty()) {
joined += ",";
}
joined += entry;
}
return joined;
}
// Posts a task that posts itself again every time it runs, the way Dawn's AsyncRunner polls for
// completed work for as long as any is outstanding.
void PostSelfRePostingTask(EventLoop* loop, int* runs) {
loop->PostTask([loop, runs] {
++*runs;
PostSelfRePostingTask(loop, runs);
});
}
// The loop runs its tasks inside a handle scope and drains microtasks between them, so it needs an
// entered isolate with an entered context even for the tests whose tasks are pure C++. Microtasks
// are explicit so that the checkpoint the loop performs between tasks is the only thing that
// drains them and a test can pin down when they run.
class EventLoopTest : public test::V8IsolateTest {
protected:
EventLoopTest() : V8IsolateTest(v8::MicrotasksPolicy::kExplicit) {}
// Installs a global record(name) that appends to `log`. The tasks in these tests are written
// in C++ and the microtasks in JavaScript, so they need one shared list to be ordered against
// each other.
void InstallRecorder(std::vector<std::string>* log) {
v8::Local<v8::External> data =
v8::External::New(isolate_, log, v8::kExternalPointerTypeTagDefault);
v8::Local<v8::Function> record = v8::FunctionTemplate::New(isolate_, Record, data)
->GetFunction(context())
.ToLocalChecked();
context()
->Global()
->Set(context(), v8::String::NewFromUtf8Literal(isolate_, "record"), record)
.Check();
}
// Evaluates `source` in the entered context. A script that throws leaves the exception pending
// rather than reporting it here, which is what lets a test hand one to the loop.
void RunScript(const char* source) {
v8::Local<v8::String> text = v8::String::NewFromUtf8(isolate_, source).ToLocalChecked();
v8::Local<v8::Script> script;
if (!v8::Script::Compile(context(), text).ToLocal(&script)) {
return;
}
static_cast<void>(script->Run(context()));
}
v8::Platform* const platform_ = test::V8Platform();
private:
static void Record(const v8::FunctionCallbackInfo<v8::Value>& info) {
auto* log = static_cast<std::vector<std::string>*>(
info.DataV2().As<v8::External>()->Value(v8::kExternalPointerTypeTagDefault));
v8::String::Utf8Value name(info.GetIsolate(), info[0]);
log->emplace_back(*name != nullptr ? *name : "");
}
};
TEST_F(EventLoopTest, ImmediatesRunInTheOrderTheyWerePosted) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostTask([&] { order.push_back("first"); });
loop.PostTask([&] { order.push_back("second"); });
loop.PostTask([&] { order.push_back("third"); });
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "first,second,third");
}
TEST_F(EventLoopTest, TimersRunInDueOrder) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostDelayedTask([&] { order.push_back("30ms"); }, milliseconds(30));
loop.PostDelayedTask([&] { order.push_back("10ms"); }, milliseconds(10));
loop.PostDelayedTask([&] { order.push_back("20ms"); }, milliseconds(20));
loop.AdvanceBy(milliseconds(30));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "10ms,20ms,30ms");
}
TEST_F(EventLoopTest, TimersWithTheSameDeadlineRunInTheOrderTheyWerePosted) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostDelayedTask([&] { order.push_back("first"); }, milliseconds(10));
loop.PostDelayedTask([&] { order.push_back("second"); }, milliseconds(10));
loop.PostDelayedTask([&] { order.push_back("third"); }, milliseconds(10));
loop.AdvanceBy(milliseconds(10));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "first,second,third");
}
TEST_F(EventLoopTest, ATimerThatIsNotYetDueDoesNotRun) {
TestEventLoop loop(isolate_, platform_);
bool ran = false;
loop.PostDelayedTask([&] { ran = true; }, milliseconds(10));
loop.AdvanceBy(milliseconds(9));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kIdle);
EXPECT_FALSE(ran);
loop.AdvanceBy(milliseconds(1));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_TRUE(ran);
}
TEST_F(EventLoopTest, ATimerArmedFromInsideTheTimersPhaseWaitsForTheNextIteration) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostDelayedTask(
[&] {
order.push_back("outer");
loop.PostDelayedTask([&] { order.push_back("inner"); }, milliseconds(0));
},
milliseconds(0));
// The clock does not move while the phase runs, so the inner timer comes due at exactly the
// phase cutoff. It must still be held over, or a repeating zero-delay timer would keep the
// loop in the timers phase indefinitely.
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "outer");
order.clear();
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "inner");
}
TEST_F(EventLoopTest, AnImmediatePostedFromInsideTheCheckPhaseWaitsForTheNextIteration) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostTask([&] {
order.push_back("outer");
loop.PostTask([&] { order.push_back("inner"); });
});
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "outer");
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "outer,inner");
}
TEST_F(EventLoopTest, ASelfRePostingImmediateDoesNotStarveTimers) {
TestEventLoop loop(isolate_, platform_);
int polls = 0;
bool timer_ran = false;
PostSelfRePostingTask(&loop, &polls);
loop.PostDelayedTask([&] { timer_ran = true; }, milliseconds(10));
// The poller runs once per iteration and never empties its queue. A loop that drained the
// check phase until it was empty would spin here forever and the timer would never fire.
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
}
EXPECT_EQ(polls, 3);
EXPECT_FALSE(timer_ran);
loop.AdvanceBy(milliseconds(10));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_TRUE(timer_ran);
EXPECT_EQ(polls, 4);
}
TEST_F(EventLoopTest, CancelDelayedTaskPreventsTheTaskFromRunning) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostDelayedTask([&] { order.push_back("kept"); }, milliseconds(10));
const EventLoop::TimerId cancelled =
loop.PostDelayedTask([&] { order.push_back("cancelled"); }, milliseconds(10));
loop.CancelDelayedTask(cancelled);
loop.AdvanceBy(milliseconds(10));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "kept");
}
TEST_F(EventLoopTest, CancellingAnUnknownOrExpiredTimerIsIgnored) {
TestEventLoop loop(isolate_, platform_);
bool ran = false;
const EventLoop::TimerId id = loop.PostDelayedTask([&] { ran = true; }, milliseconds(0));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_TRUE(ran);
// clearTimeout() on a timer that has already fired, and on one that never existed, are both
// no-ops rather than errors.
loop.CancelDelayedTask(id);
loop.CancelDelayedTask(id + 1000);
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kIdle);
}
TEST_F(EventLoopTest, ANegativeDelayIsTreatedAsZero) {
TestEventLoop loop(isolate_, platform_);
bool ran = false;
loop.PostDelayedTask([&] { ran = true; }, milliseconds(-100));
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_TRUE(ran);
}
TEST_F(EventLoopTest, AnIterationWithNothingToDoReportsIdle) {
TestEventLoop loop(isolate_, platform_);
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kIdle);
EXPECT_FALSE(loop.NextDueTime().has_value());
}
TEST_F(EventLoopTest, NextDueTimeReportsTheEarliestTimer) {
TestEventLoop loop(isolate_, platform_);
loop.PostDelayedTask([] {}, milliseconds(50));
const std::optional<EventLoop::TimePoint> later = loop.NextDueTime();
ASSERT_TRUE(later.has_value());
loop.PostDelayedTask([] {}, milliseconds(10));
const std::optional<EventLoop::TimePoint> earlier = loop.NextDueTime();
ASSERT_TRUE(earlier.has_value());
EXPECT_LT(*earlier, *later);
}
TEST_F(EventLoopTest, MicrotasksRunBetweenTasksRatherThanAfterThem) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
InstallRecorder(&order);
loop.PostTask([&] {
order.push_back("first task");
RunScript("Promise.resolve().then(() => record('microtask'));");
});
loop.PostTask([&] { order.push_back("second task"); });
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kRanTasks);
EXPECT_EQ(Join(order), "first task,microtask,second task");
}
TEST_F(EventLoopTest, AnUncaughtExceptionIsReportedAndStopsTheLoop) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostTask([&] {
order.push_back("throws");
RunScript("throw new Error('boom');");
});
loop.PostTask([&] { order.push_back("never runs"); });
StderrCapture stderr_capture;
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kStopped);
EXPECT_NE(stderr_capture.str().find("boom"), std::string::npos);
EXPECT_EQ(Join(order), "throws");
EXPECT_TRUE(loop.stopped());
EXPECT_EQ(loop.exit_code(), 1);
}
TEST_F(EventLoopTest, StopFromInsideATaskAbandonsTheRemainingImmediates) {
TestEventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostTask([&] {
order.push_back("first");
loop.Stop(0);
});
loop.PostTask([&] { order.push_back("second"); });
EXPECT_EQ(loop.RunOneIteration(), IterationResult::kStopped);
EXPECT_EQ(Join(order), "first");
}
// Run() sleeps against the real clock, so the tests below use a plain EventLoop with delays short
// enough to be imperceptible. They assert only the order things happened in, never how long
// anything took, so a loaded machine cannot fail them.
constexpr auto kShortDelay = milliseconds(1);
TEST_F(EventLoopTest, RunReturnsWhenNoWorkRemains) {
EventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostTask([&] { order.push_back("immediate"); });
loop.PostDelayedTask([&] { order.push_back("timer"); }, kShortDelay);
loop.Run();
// The immediate is ready straight away; reaching the timer means the loop waited for it.
EXPECT_EQ(Join(order), "immediate,timer");
EXPECT_FALSE(loop.stopped());
}
TEST_F(EventLoopTest, RunDrivesAChainOfTimers) {
EventLoop loop(isolate_, platform_);
std::vector<std::string> order;
loop.PostDelayedTask(
[&] {
order.push_back("first");
loop.PostDelayedTask([&] { order.push_back("second"); }, kShortDelay);
},
kShortDelay);
loop.Run();
EXPECT_EQ(Join(order), "first,second");
}
TEST_F(EventLoopTest, RunStopsAndKeepsTheExitCode) {
EventLoop loop(isolate_, platform_);
std::vector<std::string> order;
int polls = 0;
loop.PostTask([&] {
order.push_back("ran");
loop.Stop(3);
});
// Would keep the loop alive forever if Stop() were ignored.
PostSelfRePostingTask(&loop, &polls);
loop.Run();
EXPECT_EQ(Join(order), "ran");
EXPECT_EQ(polls, 0);
EXPECT_TRUE(loop.stopped());
EXPECT_EQ(loop.exit_code(), 3);
}
} // namespace
} // namespace dawn::node::standalone
#pragma clang diagnostic pop