Skip to content
File

Blob: src/workerd/tests/test-fixture.c++

15.3 KB
1// Copyright (c) 2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "test-fixture.h"
6 
7#include <workerd/api/actor-state.h>
8#include <workerd/api/global-scope.h>
9#include <workerd/api/memory-cache.h>
10#include <workerd/io/actor-cache.h>
11#include <workerd/io/actor-id.h>
12#include <workerd/io/io-channels.h>
13#include <workerd/io/limit-enforcer.h>
14#include <workerd/io/observer.h>
15#include <workerd/io/tracer.h>
16#include <workerd/jsg/jsg.h>
17#include <workerd/jsg/setup.h>
18#include <workerd/server/workerd-api.h>
19#include <workerd/util/autogate.h>
20#include <workerd/util/stream-utils.h>
21 
22#include <algorithm>
23 
24namespace workerd {
25 
26namespace {
27 
28jsg::V8System testV8System;
29 
30class MockCacheClient final: public CacheClient {
31 kj::Own<kj::HttpClient> getDefault(CacheClient::SubrequestMetadata metadata) override {
32 KJ_FAIL_REQUIRE("Not implemented");
33 }
34 
35 kj::Own<kj::HttpClient> getNamespace(
36 kj::StringPtr name, CacheClient::SubrequestMetadata metadata) override {
37 return getDefault(kj::mv(metadata));
38 }
39};
40 
41class MockTimer final: public kj::Timer {
42 kj::TimePoint now() const override {
43 return kj::systemCoarseMonotonicClock().now();
44 }
45 kj::Promise<void> atTime(kj::TimePoint time) override {
46 return kj::NEVER_DONE;
47 }
48 kj::Promise<void> afterDelay(kj::Duration delay) override {
49 return kj::NEVER_DONE;
50 }
51};
52 
53class DummyErrorHandler final: public kj::TaskSet::ErrorHandler {
54 void taskFailed(kj::Exception&& exception) override {}
55};
56 
57struct MockTimerChannel final: public TimerChannel {
58 void syncTime() override {}
59 
60 kj::Date now(kj::Maybe<kj::Date>) override {
61 return kj::systemPreciseCalendarClock().now();
62 }
63 
64 kj::Promise<void> atTime(kj::Date when) override {
65 return kj::NEVER_DONE;
66 }
67 
68 kj::Promise<void> afterLimitTimeout(kj::Duration t) override {
69 return kj::NEVER_DONE;
70 }
71};
72 
73// A TimerChannel implementation that uses real timers from the KJ event loop.
74// Useful for tests that need actual timer functionality (e.g., benchmarks with
75// simulated I/O delays).
76struct RealTimerChannel final: public TimerChannel {
77 explicit RealTimerChannel(kj::Timer& timer): timer(timer) {}
78 
79 void syncTime() override {}
80 
81 kj::Date now(kj::Maybe<kj::Date>) override {
82 return kj::systemPreciseCalendarClock().now();
83 }
84 
85 kj::Promise<void> atTime(kj::Date when) override {
86 auto nowTime = kj::systemPreciseCalendarClock().now();
87 if (when <= nowTime) {
88 return kj::READY_NOW;
89 }
90 return timer.afterDelay(when - nowTime);
91 }
92 
93 kj::Promise<void> afterLimitTimeout(kj::Duration t) override {
94 return timer.afterDelay(t);
95 }
96 
97 kj::Timer& timer;
98};
99 
100static constexpr kj::StringPtr mainModuleSource = R"SCRIPT(
101 export default {
102 fetch(request) { return new Response("OK"); },
103 };
104)SCRIPT"_kj;
105static constexpr kj::StringPtr mainModuleName = "main"_kj;
106 
107static constexpr kj::StringPtr scriptId = "script"_kj;
108 
109class MockEntropySource final: public kj::EntropySource {
110 public:
111 ~MockEntropySource() {}
112 void generate(kj::ArrayPtr<kj::byte> buffer) override {
113 for (kj::byte& b: buffer) {
114 b = counter++;
115 }
116 }
117 
118 template <typename T>
119 T rand() {
120 T r;
121 this->generate(kj::arrayPtr(&r, 1).asBytes());
122 return r;
123 }
124 
125 private:
126 kj::byte counter = 0;
127};
128 
129struct MockLimitEnforcer final: public LimitEnforcer {
130 kj::Own<void> enterJs(jsg::Lock& lock, IoContext& context) override {
131 return {};
132 }
133 void topUpActor() override {}
134 void newSubrequest(bool isInHouse) override {}
135 void newKvRequest(KvOpType op) override {}
136 void newAnalyticsEngineRequest() override {}
137 kj::Promise<void> limitDrain() override {
138 return kj::NEVER_DONE;
139 }
140 kj::Promise<void> limitScheduled() override {
141 return kj::NEVER_DONE;
142 }
143 kj::Duration getAlarmLimit() override {
144 return 15 * kj::MINUTES;
145 }
146 size_t getBufferingLimit() override {
147 return kj::maxValue;
148 }
149 kj::Maybe<EventOutcome> getLimitsExceeded() override {
150 return kj::none;
151 }
152 kj::Promise<void> onLimitsExceeded() override {
153 return kj::NEVER_DONE;
154 }
155 void setCpuLimitNearlyExceededCallback(kj::Function<void(void)> cb) override {}
156 void requireLimitsNotExceeded() override {}
157 void reportMetrics(RequestObserver& requestMetrics) override {}
158 kj::Duration consumeTimeElapsedForPeriodicLogging() override {
159 return 0 * kj::SECONDS;
160 }
161 size_t getSqliteMemoryUsage() const override {
162 return 0;
163 }
164};
165 
166struct MockIsolateLimitEnforcer final: public IsolateLimitEnforcer {
167 v8::Isolate::CreateParams getCreateParams() override {
168 return {};
169 }
170 void customizeIsolate(v8::Isolate* isolate) override {}
171 ActorCacheSharedLruOptions getActorCacheLruOptions() override {
172 return {.softLimit = 16 * (1ull << 20), // 16 MiB
173 .hardLimit = 128 * (1ull << 20), // 128 MiB
174 .staleTimeout = 30 * kj::SECONDS,
175 .dirtyListByteLimit = 8 * (1ull << 20), // 8 MiB
176 .maxKeysPerRpc = 128,
177 .neverFlush = true};
178 }
179 kj::Own<void> enterStartupJs(
180 jsg::Lock& lock, kj::OneOf<kj::Exception, kj::Duration>&) const override {
181 return {};
182 }
183 kj::Own<void> enterStartupPython(
184 jsg::Lock& lock, kj::OneOf<kj::Exception, kj::Duration>&) const override {
185 return {};
186 }
187 kj::Own<void> enterDynamicImportJs(
188 jsg::Lock& lock, kj::OneOf<kj::Exception, kj::Duration>&) const override {
189 return {};
190 }
191 kj::Own<void> enterLoggingJs(
192 jsg::Lock& lock, kj::OneOf<kj::Exception, kj::Duration>&) const override {
193 return {};
194 }
195 kj::Own<void> enterInspectorJs(
196 jsg::Lock& loc, kj::OneOf<kj::Exception, kj::Duration>&) const override {
197 return {};
198 }
199 void completedRequest(kj::StringPtr id) const override {}
200 bool exitJs(jsg::Lock& lock) const override {
201 return false;
202 }
203 void reportMetrics(IsolateObserver& isolateMetrics) const override {}
204 kj::Maybe<size_t> checkPbkdfIterations(jsg::Lock& lock, size_t iterations) const override {
205 return kj::none;
206 }
207 bool hasExcessivelyExceededHeapLimit() const override {
208 return false;
209 }
210 const TrackedWasmInstanceList& getTrackedWasmInstances() const override {
211 return trackedWasmInstances;
212 }
213 
214 private:
215 TrackedWasmInstanceList trackedWasmInstances;
216};
217 
218struct MockErrorReporter final: public Worker::ValidationErrorReporter {
219 void addError(kj::String error) override {
220 KJ_FAIL_REQUIRE("unexpected error", error);
221 }
222 
223 void addEntrypoint(kj::Maybe<kj::StringPtr> exportName, kj::Array<kj::String> methods) override {}
224 void addActorClass(kj::StringPtr exportName) override {}
225 void addWorkflowClass(kj::StringPtr exportName, kj::Array<kj::String> methods) override {}
226};
227 
228inline server::config::Worker::Reader buildConfig(
229 TestFixture::SetupParams& params, capnp::MallocMessageBuilder& arena) {
230 auto config = arena.initRoot<server::config::Worker>();
231 auto modules = config.initModules(1);
232 modules[0].setName(mainModuleName);
233 modules[0].setEsModule(params.mainModuleSource.orDefault(mainModuleSource));
234 
235 // Initialize autogates with an empty config. TODO(later): allow TestFixture to accept autogate
236 // states and pass them in here.
237 //
238 // This needs to happen here because `buildConfig` is called early in the construction of
239 // `TestFixture`.
240 util::Autogate::initAutogate({});
241 
242 return config;
243}
244 
245struct MemoryOutputStream final: kj::AsyncOutputStream, public kj::Refcounted {
246 kj::Vector<byte> content;
247 
248 kj::Promise<void> write(kj::ArrayPtr<const byte> buffer) override {
249 content.addAll(buffer);
250 return kj::READY_NOW;
251 }
252 
253 kj::Promise<void> write(kj::ArrayPtr<const kj::ArrayPtr<const byte>> pieces) override {
254 KJ_FAIL_REQUIRE("NOT IMPLEMENTED");
255 }
256 
257 kj::Promise<void> whenWriteDisconnected() override {
258 return kj::NEVER_DONE;
259 }
260 
261 kj::String str() {
262 return kj::str(content.asPtr().asChars());
263 }
264};
265 
266struct MockResponse final: public kj::HttpService::Response {
267 uint statusCode = 0;
268 kj::StringPtr statusText;
269 kj::Own<MemoryOutputStream> body = kj::refcounted<MemoryOutputStream>();
270 
271 kj::Own<kj::AsyncOutputStream> send(uint statusCode,
272 kj::StringPtr statusText,
273 const kj::HttpHeaders& headers,
274 kj::Maybe<uint64_t> expectedBodySize = kj::none) override {
275 this->statusCode = statusCode;
276 this->statusText = statusText;
277 return kj::addRef(*body);
278 }
279 
280 kj::Own<kj::WebSocket> acceptWebSocket(const kj::HttpHeaders& headers) override {
281 KJ_FAIL_REQUIRE("NOT SUPPORTED");
282 }
283};
284 
285class MockActorLoopback: public Worker::Actor::Loopback, public kj::Refcounted {
286 public:
287 kj::Own<WorkerInterface> getWorker(IoChannelFactory::SubrequestMetadata metadata) override {
288 return kj::Own<WorkerInterface>();
289 };
290 
291 kj::Own<Worker::Actor::Loopback> addRef() override {
292 return kj::addRef(*this);
293 };
294};
295 
296} // namespace
297 
298// Out-of-line because it references file-local MockCacheClient from the anonymous namespace above.
299kj::Own<CacheClient> TestFixture::DummyIoChannelFactory::getCache() {
300 return kj::heap<MockCacheClient>();
301}
302 
303using api::pyodide::PythonConfig;
304 
305const PythonConfig defaultPythonConfig{.packageDiskCacheRoot = kj::none,
306 .pyodideDiskCacheRoot = kj::none,
307 .createSnapshot = false,
308 .createBaselineSnapshot = false};
309 
310TestFixture::TestFixture(SetupParams&& params)
311 : waitScope(params.waitScope),
312 config(buildConfig(params, configArena)),
313 io(params.waitScope == kj::none ? kj::Maybe(kj::setupAsyncIo())
314 : kj::Maybe<kj::AsyncIoContext>(kj::none)),
315 timer(kj::heap<MockTimer>()),
316 timerChannel(params.useRealTimers && io != kj::none
317 ? kj::Own<TimerChannel>(
318 kj::heap<RealTimerChannel>(KJ_ASSERT_NONNULL(io).provider->getTimer()))
319 : kj::Own<TimerChannel>(kj::heap<MockTimerChannel>())),
320 entropySource(kj::heap<MockEntropySource>()),
321 threadContextHeaderBundle(headerTableBuilder),
322 httpOverCapnpFactory(byteStreamFactory,
323 capnp::HttpOverCapnpFactory::HeaderIdBundle(headerTableBuilder),
324 capnp::HttpOverCapnpFactory::LEVEL_2),
325 threadContext(*timer,
326 *entropySource,
327 threadContextHeaderBundle,
328 httpOverCapnpFactory,
329 byteStreamFactory),
330 errorReporter(kj::heap<MockErrorReporter>()),
331 memoryCacheProvider(kj::heap<api::MemoryCacheProvider>(*timer)),
332 isolateGroup(v8::IsolateGroup::GetDefault()),
333 api(kj::heap<server::WorkerdApi>(testV8System,
334 params.featureFlags.orDefault(CompatibilityFlags::Reader()),
335 capnp::List<server::config::Extension>::Reader{},
336 kj::heap<MockIsolateLimitEnforcer>()->getCreateParams(),
337 isolateGroup,
338 kj::atomicRefcounted<JsgIsolateObserver>(),
339 *memoryCacheProvider,
340 defaultPythonConfig)),
341 workerIsolate(kj::atomicRefcounted<Worker::Isolate>(kj::mv(api),
342 kj::atomicRefcounted<IsolateObserver>(),
343 scriptId,
344 kj::heap<MockIsolateLimitEnforcer>(),
345 Worker::Isolate::InspectorPolicy::DISALLOW)),
346 workerScript(kj::atomicRefcounted<Worker::Script>(kj::atomicAddRef(*workerIsolate),
347 scriptId,
348 server::WorkerdApi::extractSource(mainModuleName,
349 config,
350 params.featureFlags.orDefault(CompatibilityFlags::Reader()),
351 *errorReporter),
352 IsolateObserver::StartType::COLD,
353 false,
354 kj::none,
355 kj::none,
356 SpanParent(nullptr),
357 newWorkerFileSystem(kj::heap<FsMap>(), getTmpDirectoryImpl()),
358 kj::none /* new module registry */)),
359 worker(kj::atomicRefcounted<Worker>(kj::atomicAddRef(*workerScript),
360 kj::atomicRefcounted<WorkerObserver>(),
361 [](jsg::Lock&, const Worker::Api&, v8::Local<v8::Object>, v8::Local<v8::Object>) {
362 // no bindings, nothing to do
363 },
364 IsolateObserver::StartType::COLD,
365 SpanParent(nullptr),
366 Worker::LockType(Worker::Lock::TakeSynchronously(kj::none)))),
367 errorHandler(kj::heap<DummyErrorHandler>()),
368 waitUntilTasks(*errorHandler),
369 headerTable(headerTableBuilder.build()),
370 ioChannelFactory(kj::mv(params.ioChannelFactory)) {
371 KJ_IF_SOME(id, params.actorId) {
372 auto makeActorCache = [](const ActorCache::SharedLru& sharedLru, OutputGate& outputGate,
373 ActorCache::Hooks& hooks, SqliteObserver& sqliteObserver) {
374 return kj::heap<ActorCache>(
375 server::newEmptyReadOnlyActorStorage(), sharedLru, outputGate, hooks);
376 };
377 auto makeStorage = [](jsg::Lock& js, const Worker::Api& api,
378 ActorCacheInterface& actorCache) -> jsg::Ref<api::DurableObjectStorage> {
379 return js.alloc<api::DurableObjectStorage>(
380 js, IoContext::current().addObject(actorCache), /*enableSql=*/false);
381 };
382 actor = kj::refcounted<Worker::Actor>(*worker, /*tracker=*/kj::none, kj::mv(id),
383 /*hasTransient=*/false, makeActorCache,
384 /*classname=*/kj::none, /*props=*/Frankenvalue(), makeStorage,
385 kj::refcounted<MockActorLoopback>(), *timerChannel, kj::refcounted<ActorObserver>(),
386 kj::none, kj::none);
387 }
388}
389 
390void TestFixture::runInIoContext(kj::Function<kj::Promise<void>(const Environment&)>&& callback,
391 const kj::ArrayPtr<const kj::StringPtr> errorsToIgnore) {
392 auto ignoreDescription = [&errorsToIgnore](kj::StringPtr description) {
393 return std::any_of(errorsToIgnore.begin(), errorsToIgnore.end(),
394 [&description](auto error) { return description.contains(error); });
395 };
396 
397 try {
398 runInIoContext([callback = kj::mv(callback), &ignoreDescription](
399 const TestFixture::Environment& env) mutable -> kj::Promise<void> {
400 v8::TryCatch tryCatch(env.isolate);
401 try {
402 return callback(env);
403 } catch (jsg::JsExceptionThrown&) {
404 if (!tryCatch.CanContinue()) {
405 throw;
406 }
407 if (ignoreDescription(kj::str(tryCatch.Exception()))) {
408 return kj::READY_NOW;
409 }
410 tryCatch.ReThrow();
411 throw;
412 }
413 });
414 } catch (kj::Exception& e) {
415 if (!ignoreDescription(e.getDescription())) {
416 throw kj::mv(e);
417 }
418 }
419}
420 
421kj::Own<IoContext::IncomingRequest> TestFixture::createIncomingRequest() {
422 auto context = kj::refcounted<IoContext>(
423 threadContext, kj::atomicAddRef(*worker), actor, kj::heap<MockLimitEnforcer>());
424 kj::Own<IoChannelFactory> channelFactory;
425 KJ_IF_SOME(factory, ioChannelFactory) {
426 channelFactory = factory(*timerChannel);
427 } else {
428 channelFactory = kj::heap<DummyIoChannelFactory>(*timerChannel);
429 }
430 auto incomingRequest = kj::heap<IoContext::IncomingRequest>(kj::addRef(*context),
431 kj::mv(channelFactory), kj::refcounted<RequestObserver>(), kj::none, kj::none);
432 incomingRequest->delivered();
433 return incomingRequest;
434}
435 
436TestFixture::Response TestFixture::runRequest(
437 kj::HttpMethod method, kj::StringPtr url, kj::StringPtr body) {
438 kj::HttpHeaders requestHeaders(*headerTable);
439 MockResponse response;
440 auto requestBody = newMemoryInputStream(body);
441 
442 runInIoContext([&](const TestFixture::Environment& env) {
443 auto& globalScope = env.lock.getGlobalScope();
444 return globalScope.request(method, url, requestHeaders, *requestBody, response, "{}"_kj,
445 env.lock,
446 env.lock.getExportedHandler(/*entryPointName=*/kj::none, /*versionInfo=*/kj::none,
447 /*props=*/{}, /*actor=*/kj::none),
448 /*abortSignal=*/kj::none);
449 });
450 
451 return {.statusCode = response.statusCode, .body = response.body->str()};
452}
453 
454} // namespace workerd