| // 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 |