// 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[] = []; 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(); 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(); 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", ); });