deno/tests/unit/cron_test.ts
Matt Mastracci f5e46c9bf2
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests ->
tests, and updates of relative paths for files.

This is the first step towards aggregate all of the integration test
files under tests/, which will lead to a set of integration tests that
can run without the CLI binary being built.

While we could leave these tests under `cli`, it would require us to
keep a more complex directory structure for the various test runners. In
addition, we have a lot of complexity to ignore various test files in
the `cli` project itself (cargo publish exclusion rules, autotests =
false, etc).

And finally, the `tests/` folder will eventually house the `test_ffi`,
`test_napi` and other testing code, reducing the size of the root repo
directory.

For easier review, the extremely large and noisy "move" is in the first
commit (with no changes -- just a move), while the remainder of the
changes to actual files is in the second commit.
2024-02-10 20:22:13 +00:00

461 lines
10 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals, assertThrows } from "./test_util.ts";
// @ts-ignore This is not publicly typed namespace, but it's there for sure.
const {
formatToCronSchedule,
parseScheduleToString,
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
} = Deno[Deno.internal];
const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
Deno.test(function noNameTest() {
assertThrows(
// @ts-ignore test
() => Deno.cron(),
TypeError,
"Deno.cron requires a unique name",
);
});
Deno.test(function noSchedule() {
assertThrows(
// @ts-ignore test
() => Deno.cron("foo"),
TypeError,
"Deno.cron requires a valid schedule",
);
});
Deno.test(function noHandler() {
assertThrows(
// @ts-ignore test
() => Deno.cron("foo", "*/1 * * * *"),
TypeError,
"Deno.cron requires a handler",
);
});
Deno.test(function invalidNameTest() {
assertThrows(
() => Deno.cron("abc[]", "*/1 * * * *", () => {}),
TypeError,
"Invalid cron name",
);
assertThrows(
() => Deno.cron("a**bc", "*/1 * * * *", () => {}),
TypeError,
"Invalid cron name",
);
assertThrows(
() => Deno.cron("abc<>", "*/1 * * * *", () => {}),
TypeError,
"Invalid cron name",
);
assertThrows(
() => Deno.cron(";']", "*/1 * * * *", () => {}),
TypeError,
"Invalid cron name",
);
assertThrows(
() =>
Deno.cron(
"0000000000000000000000000000000000000000000000000000000000000000000000",
"*/1 * * * *",
() => {},
),
TypeError,
"Cron name is too long",
);
});
Deno.test(function invalidScheduleTest() {
assertThrows(
() => Deno.cron("abc", "bogus", () => {}),
TypeError,
"Invalid cron schedule",
);
assertThrows(
() => Deno.cron("abc", "* * * * * *", () => {}),
TypeError,
"Invalid cron schedule",
);
assertThrows(
() => Deno.cron("abc", "* * * *", () => {}),
TypeError,
"Invalid cron schedule",
);
assertThrows(
() => Deno.cron("abc", "m * * * *", () => {}),
TypeError,
"Invalid cron schedule",
);
});
Deno.test(function invalidBackoffScheduleTest() {
assertThrows(
() =>
Deno.cron(
"abc",
"*/1 * * * *",
{ backoffSchedule: [1, 1, 1, 1, 1, 1] },
() => {},
),
TypeError,
"Invalid backoff schedule",
);
assertThrows(
() =>
Deno.cron("abc", "*/1 * * * *", { backoffSchedule: [3600001] }, () => {}),
TypeError,
"Invalid backoff schedule",
);
});
Deno.test(async function tooManyCrons() {
const crons: Promise<void>[] = [];
const ac = new AbortController();
for (let i = 0; i <= 100; i++) {
const c = Deno.cron(
`abc_${i}`,
"*/1 * * * *",
{ signal: ac.signal },
() => {},
);
crons.push(c);
}
try {
assertThrows(
() => {
Deno.cron("next-cron", "*/1 * * * *", { signal: ac.signal }, () => {});
},
TypeError,
"Too many crons",
);
} finally {
ac.abort();
for (const c of crons) {
await c;
}
}
});
Deno.test(async function duplicateCrons() {
const ac = new AbortController();
const c = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => {});
try {
assertThrows(
() => Deno.cron("abc", "*/20 * * * *", () => {}),
TypeError,
"Cron with this name already exists",
);
} finally {
ac.abort();
await c;
}
});
Deno.test(async function basicTest() {
Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
let count = 0;
const { promise, resolve } = Promise.withResolvers<void>();
const ac = new AbortController();
const c = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => {
count++;
if (count > 5) {
resolve();
}
});
try {
await promise;
} finally {
ac.abort();
await c;
}
});
Deno.test(async function basicTestWithJsonFormatScheduleExpression() {
Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
let count = 0;
const { promise, resolve } = Promise.withResolvers<void>();
const ac = new AbortController();
const c = Deno.cron(
"abc",
{ minute: { every: 20 } },
{ signal: ac.signal },
() => {
count++;
if (count > 5) {
resolve();
}
},
);
try {
await promise;
} finally {
ac.abort();
await c;
}
});
Deno.test(async function multipleCrons() {
Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
let count0 = 0;
let count1 = 0;
const { promise: promise0, resolve: resolve0 } = Promise.withResolvers<
void
>();
const { promise: promise1, resolve: resolve1 } = Promise.withResolvers<
void
>();
const ac = new AbortController();
const c0 = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => {
count0++;
if (count0 > 5) {
resolve0();
}
});
const c1 = Deno.cron("xyz", "*/20 * * * *", { signal: ac.signal }, () => {
count1++;
if (count1 > 5) {
resolve1();
}
});
try {
await promise0;
await promise1;
} finally {
ac.abort();
await c0;
await c1;
}
});
Deno.test(async function overlappingExecutions() {
Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
let count = 0;
const { promise: promise0, resolve: resolve0 } = Promise.withResolvers<
void
>();
const { promise: promise1, resolve: resolve1 } = Promise.withResolvers<
void
>();
const ac = new AbortController();
const c = Deno.cron(
"abc",
"*/20 * * * *",
{ signal: ac.signal },
async () => {
resolve0();
count++;
await promise1;
},
);
try {
await promise0;
} finally {
await sleep(2000);
resolve1();
ac.abort();
await c;
}
assertEquals(count, 1);
});
Deno.test(async function retriesWithBackoffSchedule() {
Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "5000");
let count = 0;
const ac = new AbortController();
const c = Deno.cron("abc", "*/20 * * * *", {
signal: ac.signal,
backoffSchedule: [10, 20],
}, async () => {
count += 1;
await sleep(10);
throw new TypeError("cron error");
});
try {
await sleep(6000);
} finally {
ac.abort();
await c;
}
// The cron should have executed 3 times (1st attempt and 2 retries).
assertEquals(count, 3);
});
Deno.test(async function retriesWithBackoffScheduleOldApi() {
Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "5000");
let count = 0;
const ac = new AbortController();
const c = Deno.cron("abc2", "*/20 * * * *", {
signal: ac.signal,
backoffSchedule: [10, 20],
}, async () => {
count += 1;
await sleep(10);
throw new TypeError("cron error");
});
try {
await sleep(6000);
} finally {
ac.abort();
await c;
}
// The cron should have executed 3 times (1st attempt and 2 retries).
assertEquals(count, 3);
});
Deno.test("formatToCronSchedule - undefined value", () => {
const result = formatToCronSchedule();
assertEquals(result, "*");
});
Deno.test("formatToCronSchedule - number value", () => {
const result = formatToCronSchedule(5);
assertEquals(result, "5");
});
Deno.test("formatToCronSchedule - exact array value", () => {
const result = formatToCronSchedule({ exact: [1, 2, 3] });
assertEquals(result, "1,2,3");
});
Deno.test("formatToCronSchedule - exact number value", () => {
const result = formatToCronSchedule({ exact: 5 });
assertEquals(result, "5");
});
Deno.test("formatToCronSchedule - start, end, every values", () => {
const result = formatToCronSchedule({ start: 1, end: 10, every: 2 });
assertEquals(result, "1-10/2");
});
Deno.test("formatToCronSchedule - start, end values", () => {
const result = formatToCronSchedule({ start: 1, end: 10 });
assertEquals(result, "1-10");
});
Deno.test("formatToCronSchedule - start, every values", () => {
const result = formatToCronSchedule({ start: 1, every: 2 });
assertEquals(result, "1/2");
});
Deno.test("formatToCronSchedule - start value", () => {
const result = formatToCronSchedule({ start: 1 });
assertEquals(result, "1/1");
});
Deno.test("formatToCronSchedule - end, every values", () => {
assertThrows(
() => formatToCronSchedule({ end: 10, every: 2 }),
TypeError,
"Invalid cron schedule",
);
});
Deno.test("Parse CronSchedule to string", () => {
const result = parseScheduleToString({
minute: { exact: [1, 2, 3] },
hour: { start: 1, end: 10, every: 2 },
dayOfMonth: { exact: 5 },
month: { start: 1, end: 10 },
dayOfWeek: { start: 1, every: 2 },
});
assertEquals(result, "1,2,3 1-10/2 5 1-10 1/2");
});
Deno.test("Parse schedule to string - string", () => {
const result = parseScheduleToString("* * * * *");
assertEquals(result, "* * * * *");
});
Deno.test("error on two handlers", () => {
assertThrows(
() => {
// @ts-ignore test
Deno.cron("abc", "* * * * *", () => {}, () => {});
},
TypeError,
"Deno.cron requires a single handler",
);
});
Deno.test("Parse test", () => {
assertEquals(
parseScheduleToString({
minute: 3,
}),
"3 * * * *",
);
assertEquals(
parseScheduleToString({
hour: { every: 2 },
}),
"0 */2 * * *",
);
assertEquals(
parseScheduleToString({
dayOfMonth: { every: 10 },
}),
"0 0 */10 * *",
);
assertEquals(
parseScheduleToString({
month: { every: 3 },
}),
"0 0 1 */3 *",
);
assertEquals(
parseScheduleToString({
dayOfWeek: { every: 2 },
}),
"0 0 * * */2",
);
assertEquals(
parseScheduleToString({
minute: 3,
hour: { every: 2 },
}),
"3 */2 * * *",
);
assertEquals(
parseScheduleToString({
dayOfMonth: { start: 1, end: 10 },
}),
"0 0 1-10 * *",
);
assertEquals(
parseScheduleToString({
minute: { every: 10 },
dayOfMonth: { every: 5 },
}),
"*/10 * */5 * *",
);
assertEquals(
parseScheduleToString({
hour: { every: 3 },
month: { every: 2 },
}),
"0 */3 * */2 *",
);
assertEquals(
parseScheduleToString({
minute: { every: 5 },
month: { every: 2 },
}),
"*/5 * * */2 *",
);
});