Skip to content

Commit

Permalink
use memfs in unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
saiichihashimoto committed Jan 15, 2024
1 parent e603e7e commit cae6f1b
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 190 deletions.
9 changes: 8 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,14 @@
{
"autoFix": false,
"cspell": {
"words": ["cronstrue", "encryptor", "pino", "unstash", "zeplo"]
"words": [
"cronstrue",
"encryptor",
"memfs",
"pino",
"unstash",
"zeplo"
]
}
}
],
Expand Down
293 changes: 243 additions & 50 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Buffer } from "node:buffer";
import fsNative from "node:fs";
import { setTimeout } from "node:timers/promises";

// import { setTimeout } from "node:timers/promises";
import {
afterEach,
beforeEach,
Expand All @@ -9,90 +10,282 @@ import {
it,
jest,
} from "@jest/globals";
import { memfs } from "memfs";
import type { SpiedFunction } from "jest-mock";
import { memfs as memfsNative } from "memfs";

import { main } from ".";

const setTimeout = (
delay?: number,
{ signal }: { signal?: AbortSignal } = {}
) =>
new Promise<void>((resolve) => {
global.setTimeout(resolve, delay);
const memfs = (...args: Parameters<typeof memfsNative>) => {
const { fs, vol } = memfsNative(...args);

signal?.addEventListener("abort", () => resolve());
});
return {
vol,
fs: {
...fs,
/* eslint-disable promise/prefer-await-to-callbacks -- HACK */
readFile: (
path: string,
callback: (err: NodeJS.ErrnoException | null, data: Buffer) => void
) => {
try {
callback(null, fs.readFileSync(path) as Buffer);
} catch (error) {
callback(error as NodeJS.ErrnoException, Buffer.from([]));
}
},
watch: (path: any, options: { signal?: AbortSignal }, handler: any) => {
const watcher = fs.watch(path, options as any, handler);

const closeBefore = watcher.close.bind(watcher);

// TODO PR For https://github.com/streamich/memfs/blob/eac1ce29b7aa0a18b3b20d7f4821c020526420ee/src/volume.ts#L2573
watcher.close = () => {
closeBefore();
watcher.emit("close");
};

// TODO PR For this
options.signal?.addEventListener("abort", () => {
watcher.close();
});

return watcher;
},
/* eslint-enable promise/prefer-await-to-callbacks */
} as unknown as typeof fsNative,
};
};

describe("main", () => {
let controller: AbortController;
let proc: ReturnType<typeof main> | undefined;
let fetchSpy: SpiedFunction<typeof global.fetch>;
const destination = {
logs: [] as any[],
clear: () => {
destination.logs = [];
},
write: (msg: string) => {
destination.logs.push(JSON.parse(msg));
},
};

beforeEach(() => {
controller = new AbortController();
fetchSpy = jest.spyOn(global, "fetch").mockImplementation(
async (url) =>
({
headers: {},
ok: true,
redirected: false,
status: 200,
statusText: "OK",
type: "basic",
url: url.toString(),
text: async () => "",
} as Response)
);
});

afterEach(() => {
afterEach(async () => {
controller.abort();

await proc;

jest.clearAllTimers();
destination.clear();
});

it("runs continuously", async () => {
const { fs } = memfs({
[process.cwd()]: {
"./vercel.json": "{}",
},
it("runs forever", async () => {
const { fs } = memfs(
{ "./vercel.json": JSON.stringify({}) },
process.cwd()
);

proc = main({
destination,
fs,
signal: controller.signal,
});

await jest.advanceTimersByTimeAsync(0);

const [winner] = await Promise.all([
Promise.race([
(async () => {
await setTimeout(500, { signal: controller.signal });

return "timeout";
})(),
(async () => {
await main({
fs: fs as unknown as typeof fsNative,
level: "silent",
signal: controller.signal,
});

return "main";
})(),
setTimeout(500, "timeout", { signal: controller.signal }),
proc,
]),
jest.advanceTimersByTimeAsync(500),
]);

expect(winner).toBe("timeout");
expect(destination.logs).toContainEqual({
level: 30,
time: 1696486441293,
msg: "No CRONs Scheduled",
});
});

it("dry ends the process immediately", async () => {
const { fs } = memfs({
[process.cwd()]: {
"./vercel.json": "{}",
},
const { fs } = memfs(
{ "./vercel.json": JSON.stringify({}) },
process.cwd()
);

proc = main({
destination,
fs,
signal: controller.signal,
dry: true,
});

await jest.advanceTimersByTimeAsync(0);

const [winner] = await Promise.all([
Promise.race([
(async () => {
await setTimeout(500, { signal: controller.signal });

return "timeout";
})(),
(async () => {
await main({
fs: fs as unknown as typeof fsNative,
level: "silent",
signal: controller.signal,
dry: true,
});

return "main";
})(),
setTimeout(500, "timeout", { signal: controller.signal }),
proc,
]),
jest.advanceTimersByTimeAsync(500),
]);

expect(winner).toBe("main");
expect(winner).not.toBe("timeout");
expect(destination.logs).toContainEqual({
level: 30,
time: 1696486441293,
msg: "No CRONs Scheduled",
});
});

it("executes CRON when schedule passes", async () => {
const { fs } = memfs(
{
"./vercel.json": JSON.stringify({
crons: [{ path: "/some-api", schedule: "* * * * * *" }],
}),
},
process.cwd()
);

proc = main({
destination,
fs,
signal: controller.signal,
});

await jest.advanceTimersByTimeAsync(0);

expect(fetchSpy).not.toHaveBeenCalled();
expect(destination.logs).toContainEqual({
level: 30,
msg: "Scheduled /some-api Every second",
time: 1696486441293,
});
destination.clear();

await jest.advanceTimersByTimeAsync(1000);

expect(fetchSpy).toHaveBeenNthCalledWith(
1,
"http://localhost:3000/some-api",
expect.objectContaining({
method: "GET",
redirect: "manual",
headers: {},
})
);
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(destination.logs).toContainEqual({
currentRun: "2023-10-05T06:14:02.000Z",
level: 30,
msg: "Started /some-api Every second",
time: 1696486442000,
});
expect(destination.logs).toContainEqual({
currentRun: "2023-10-05T06:14:02.000Z",
level: 30,
msg: "Succeeded /some-api Every second",
status: 200,
text: "",
time: 1696486442000,
});
destination.clear();

await jest.advanceTimersByTimeAsync(1000);

expect(fetchSpy).toHaveBeenNthCalledWith(
2,
"http://localhost:3000/some-api",
expect.objectContaining({
method: "GET",
redirect: "manual",
headers: {},
})
);
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(destination.logs).toContainEqual({
currentRun: "2023-10-05T06:14:03.000Z",
level: 30,
msg: "Started /some-api Every second",
time: 1696486443000,
});
expect(destination.logs).toContainEqual({
currentRun: "2023-10-05T06:14:03.000Z",
level: 30,
msg: "Succeeded /some-api Every second",
status: 200,
text: "",
time: 1696486443000,
});
});

it("misses CRON if config changes", async () => {
const { fs } = memfs(
{
"./vercel.json": JSON.stringify({
crons: [{ path: "/some-api", schedule: "* * * * * *" }],
}),
},
process.cwd()
);

proc = main({
destination,
fs,
level: "trace",
signal: controller.signal,
});

await jest.advanceTimersByTimeAsync(0);

expect(fetchSpy).not.toHaveBeenCalled();
expect(destination.logs).toContainEqual({
level: 30,
msg: "Scheduled /some-api Every second",
time: 1696486441293,
});
destination.clear();

fs.writeFileSync("./vercel.json", JSON.stringify({}));
await jest.advanceTimersByTimeAsync(0);

expect(destination.logs).toContainEqual({
config: "./vercel.json",
level: 30,
msg: "Config Changed",
time: 1696486441293,
});
expect(destination.logs).toContainEqual({
level: 30,
msg: "No CRONs Scheduled",
time: 1696486441293,
});
destination.clear();

await jest.advanceTimersByTimeAsync(100000);

expect(fetchSpy).not.toHaveBeenCalled();
expect(fetchSpy).toHaveBeenCalledTimes(0);
expect(destination.logs).toHaveLength(0);
});
});
Loading

0 comments on commit cae6f1b

Please sign in to comment.