Skip to content

Commit

Permalink
add remote-mode to startWorker e2e tests (#6483)
Browse files Browse the repository at this point in the history
* cleanup the expect.{arrayContaining|objectContaining} calls with a custom matcher
  • Loading branch information
RamIdeas authored Aug 16, 2024
1 parent c44ab92 commit 14fb034
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 138 deletions.
252 changes: 136 additions & 116 deletions packages/wrangler/e2e/startWorker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import WebSocket from "ws";
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
import type { DevToolsEvent } from "../src/api";

const OPTIONS = [
{ remote: false },
// { remote: true },
] as const;
const OPTIONS = [{ remote: false }, { remote: true }] as const;

type Wrangler = Awaited<ReturnType<WranglerE2ETestHelper["importWrangler"]>>;

Expand Down Expand Up @@ -43,7 +40,7 @@ function collectMessagesContaining<T>(
return collection;
}

describe.each(OPTIONS)("DevEnv $remote", ({ remote }) => {
describe.each(OPTIONS)("DevEnv (remote: $remote)", ({ remote }) => {
let helper: WranglerE2ETestHelper;
let wrangler: Wrangler;
let startWorker: Wrangler["unstable_startWorker"];
Expand Down Expand Up @@ -118,10 +115,8 @@ describe.each(OPTIONS)("DevEnv $remote", ({ remote }) => {
const ws = new WebSocket(inspectorUrl.href);
const openPromise = events.once(ws, "open");

const consoleAPICalledPromise = waitForMessageContaining(
ws,
"Runtime.consoleAPICalled"
);
const consoleApiMessages: DevToolsEvent<"Runtime.consoleAPICalled">[] =
collectMessagesContaining(ws, "Runtime.consoleAPICalled");
const executionContextCreatedPromise = waitForMessageContaining(
ws,
"Runtime.executionContextCreated"
Expand All @@ -130,20 +125,23 @@ describe.each(OPTIONS)("DevEnv $remote", ({ remote }) => {
await openPromise;
await worker.fetch("http://dummy");

await expect(consoleAPICalledPromise).resolves.toMatchObject({
method: "Runtime.consoleAPICalled",
params: {
args: expect.arrayContaining([
{ type: "string", value: "Inside mock user worker" },
]),
},
});
await expect(executionContextCreatedPromise).resolves.toMatchObject({
method: "Runtime.executionContextCreated",
params: {
context: { id: expect.any(Number) },
},
});
await vi.waitFor(
() => {
expect(consoleApiMessages).toContainMatchingObject({
method: "Runtime.consoleAPICalled",
params: expect.objectContaining({
args: [{ type: "string", value: "Inside mock user worker" }],
}),
});
},
{ timeout: 5_000 }
);

// Ensure execution contexts cleared on reload
const executionContextClearedPromise = waitForMessageContaining(
Expand Down Expand Up @@ -248,23 +246,22 @@ describe.each(OPTIONS)("DevEnv $remote", ({ remote }) => {
await worker.fetch("http://dummy");

await vi.waitFor(
async () => {
await expect(consoleApiMessages).toMatchObject([
{
method: "Runtime.consoleAPICalled",
params: {
args: expect.arrayContaining([
{ type: "string", value: expect.stringContaining("zzzzzzzzz") },
]),
},
},
]);
() => {
expect(consoleApiMessages).toContainMatchingObject({
method: "Runtime.consoleAPICalled",
params: expect.objectContaining({
args: [
{ type: "string", value: expect.stringContaining("zzzzzzzzz") },
],
}),
});
},
{ timeout: 5_000 }
);
});

it("User worker exception", async (t) => {
// local only: miniflare catches error responses and pretty-prints them
it.skipIf(remote)("User worker exception", async (t) => {
t.onTestFinished(() => worker?.dispose());

await helper.seed({
Expand Down Expand Up @@ -428,8 +425,28 @@ describe.each(OPTIONS)("DevEnv $remote", ({ remote }) => {
let res = await worker.fetch("http://dummy");
let resText = await res.text();
expect(resText).toEqual(expect.stringContaining("body:1"));
expect(resText).toEqual(expect.stringMatching(scriptRegex));
expect(resText).toMatch(scriptRegex);
expect(resText.replace(scriptRegex, "").trim()).toEqual("body:1"); // test, without the <script> tag, the response is as authored
expect(resText.match(scriptRegex)?.[0]).toBe(dedent`
<script defer type="application/javascript">
(function() {
var ws;
function recover() {
ws = null;
setTimeout(initLiveReload, 100);
}
function initLiveReload() {
if (ws) return;
var origin = (location.protocol === "http:" ? "ws://" : "wss://") + location.host;
ws = new WebSocket(origin + "/cdn-cgi/live-reload", "WRANGLER_PROXYWORKER_LIVE_RELOAD_PROTOCOL");
ws.onclose = recover;
ws.onerror = recover;
ws.onmessage = location.reload.bind(location);
}
initLiveReload();
})();
</script>
`);

await helper.seed({
"src/index.ts": dedent`
Expand Down Expand Up @@ -473,112 +490,115 @@ describe.each(OPTIONS)("DevEnv $remote", ({ remote }) => {
expect(resText).not.toEqual(expect.stringMatching(scriptRegex));
});

it("urlOverrides take effect in the UserWorker", async (t) => {
t.onTestFinished(() => worker?.dispose());
// local only: origin overrides cannot be applied in remote mode
it.skipIf(remote)(
"origin override takes effect in the UserWorker",
async (t) => {
t.onTestFinished(() => worker?.dispose());

await helper.seed({
"src/index.ts": dedent`
export default {
fetch(request) {
return new Response("URL: " + request.url);
await helper.seed({
"src/index.ts": dedent`
export default {
fetch(request) {
return new Response("URL: " + request.url);
}
}
}
`,
});
`,
});

const worker = await startWorker({
name: "test-worker",
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
const worker = await startWorker({
name: "test-worker",
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),

dev: {
remote,
origin: {
hostname: "www.google.com",
dev: {
remote,
origin: {
hostname: "www.google.com",
},
},
},
});

let res = await worker.fetch("http://dummy/test/path/1");
await expect(res.text()).resolves.toBe(
`URL: http://www.google.com/test/path/1`
);

await worker.patchConfig({
dev: {
...worker.config.dev,
origin: {
secure: true,
hostname: "mybank.co.uk",
});

let res = await worker.fetch("http://dummy/test/path/1");
await expect(res.text()).resolves.toBe(
`URL: http://www.google.com/test/path/1`
);

await worker.patchConfig({
dev: {
...worker.config.dev,
origin: {
secure: true,
hostname: "mybank.co.uk",
},
},
},
});
});

res = await worker.fetch("http://dummy/test/path/2");
await expect(res.text()).resolves.toBe(
"URL: https://mybank.co.uk/test/path/2"
);
});
res = await worker.fetch("http://dummy/test/path/2");
await expect(res.text()).resolves.toBe(
"URL: https://mybank.co.uk/test/path/2"
);
}
);

it("inflight requests are retried during UserWorker reloads", async (t) => {
// to simulate inflight requests failing during UserWorker reloads,
// we will use a UserWorker with a longish `await setTimeout(...)`
// so that we can guarantee the race condition is hit
// when workerd is eventually terminated
// local only: remote workers are not terminated during reloads
it.skipIf(remote)(
"inflight requests are retried during UserWorker reloads",
async (t) => {
// to simulate inflight requests failing during UserWorker reloads,
// we will use a UserWorker with a longish `await setTimeout(...)`
// so that we can guarantee the race condition is hit when workerd is eventually terminated
// this does not apply to remote workers as they are not terminated during reloads

t.onTestFinished(() => worker?.dispose());
t.onTestFinished(() => worker?.dispose());

const script = dedent`
export default {
async fetch(request) {
const url = new URL(request.url);
const script = dedent`
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/long') {
await new Promise(r => setTimeout(r, 30_000));
}
if (url.pathname === '/long') {
await new Promise(r => setTimeout(r, 30_000));
}
return new Response("UserWorker:1");
return new Response("UserWorker:1");
}
}
}
`;
`;

await helper.seed({
"src/index.ts": script,
});
await helper.seed({
"src/index.ts": script,
});

const worker = await startWorker({
name: "test-worker",
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),
const worker = await startWorker({
name: "test-worker",
entrypoint: path.resolve(helper.tmpPath, "src/index.ts"),

dev: {
remote,
origin: {
hostname: "www.google.com",
},
},
});
dev: { remote },
});

let res = await worker.fetch("http://dummy/short");
await expect(res.text()).resolves.toBe("UserWorker:1");
let res = await worker.fetch("http://dummy/short");
await expect(res.text()).resolves.toBe("UserWorker:1");

const inflightDuringReloads = worker.fetch("http://dummy/long"); // NOTE: no await
const inflightDuringReloads = worker.fetch("http://dummy/long"); // NOTE: no await

// this will cause workerd for UserWorker:1 to terminate (eventually, but soon)
await helper.seed({
"src/index.ts": script.replace("UserWorker:1", "UserWorker:2"),
});
await setTimeout(300);
// this will cause workerd for UserWorker:1 to terminate (eventually, but soon)
await helper.seed({
"src/index.ts": script.replace("UserWorker:1", "UserWorker:2"),
});
await setTimeout(300);

res = await worker.fetch("http://dummy/short");
await expect(res.text()).resolves.toBe("UserWorker:2");
res = await worker.fetch("http://dummy/short");
await expect(res.text()).resolves.toBe("UserWorker:2");

// this will cause workerd for UserWorker:2 to terminate (eventually, but soon)
await helper.seed({
"src/index.ts": script
.replace("UserWorker:1", "UserWorker:3") // change response so it can be identified
.replace("30_000", "0"), // remove the long wait as we won't reload this UserWorker
});
// this will cause workerd for UserWorker:2 to terminate (eventually, but soon)
await helper.seed({
"src/index.ts": script
.replace("UserWorker:1", "UserWorker:3") // change response so it can be identified
.replace("30_000", "0"), // remove the long wait as we won't reload this UserWorker
});

res = await inflightDuringReloads;
await expect(res.text()).resolves.toBe("UserWorker:3");
});
res = await inflightDuringReloads;
await expect(res.text()).resolves.toBe("UserWorker:3");
}
);
});
1 change: 1 addition & 0 deletions packages/wrangler/e2e/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export default defineConfig({
// see the full string to help figure out why the assertion failed
truncateThreshold: 1e6,
},
setupFiles: ["./e2e/vitest.setup.ts"],
},
});
35 changes: 35 additions & 0 deletions packages/wrangler/e2e/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from "vitest";

interface CustomMatchers {
toContainMatchingObject: (expected: object) => unknown;
}

declare module "vitest" {
interface Assertion extends CustomMatchers {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}

expect.extend({
// Extend vitest's expect object so that it has our new matcher
toContainMatchingObject(received: object[], expected: object) {
// const matcher = createMatcher(expected);
const pass = received.some((item) => isSubset(expected, item));

return {
message: () => `Entry was${this.isNot ? "" : " not"} found in array.`,
pass,
actual: received,
expected: expected,
};

function isSubset(subset: object, superset: object) {
// Leverage the existing toMatchObject() behaviour to do the deep matching
try {
expect(superset).toMatchObject(subset);
return true;
} catch (ex) {
return false;
}
}
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export class LocalRuntimeController extends RuntimeController {
await convertToConfigBundle(data),
this.#proxyToUserWorkerAuthenticationSecret
);
options.liveReload = false; // TODO: set in buildMiniflareOptions once old code path is removed
if (this.#mf === undefined) {
logger.log(chalk.dim("⎔ Starting local server..."));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export class ProxyController extends Controller<ProxyControllerEventMap> {
logger.loggerLevel === "debug" ? "wrangler-ProxyWorker" : "wrangler",
}),
handleRuntimeStdio,
liveReload: false,
};

const proxyWorkerOptionsChanged = didMiniflareOptionsChange(
Expand Down
Loading

0 comments on commit 14fb034

Please sign in to comment.