Files
grafana/public/app/core/utils/timeRegions.test.ts
Leon Sorokin 59280d5242 Time regions: Add option for cron syntax to support complex schedules (#99548)
Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
2025-02-20 14:50:32 -06:00

381 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Duration } from 'date-fns';
import { AbsoluteTimeRange, dateTimeForTimeZone, reverseParseDuration, TimeRange } from '@grafana/data';
import { convertToCron, TimeRegionConfig } from 'app/core/utils/timeRegions';
import { calculateTimesWithin } from './timeRegions';
// random from the interwebs
function durationFromSeconds(seconds: number): Duration {
const secondsInYear = 31536000;
const secondsInMonth = 2628000;
const secondsInDay = 86400;
const secondsInHour = 3600;
const secondsInMinute = 60;
let years = Math.floor(seconds / secondsInYear);
let remainingSeconds = seconds % secondsInYear;
let months = Math.floor(remainingSeconds / secondsInMonth);
remainingSeconds %= secondsInMonth;
let days = Math.floor(remainingSeconds / secondsInDay);
remainingSeconds %= secondsInDay;
let hours = Math.floor(remainingSeconds / secondsInHour);
remainingSeconds %= secondsInHour;
let minutes = Math.floor(remainingSeconds / secondsInMinute);
let finalSeconds = remainingSeconds % secondsInMinute;
return {
years,
months,
days,
hours,
minutes,
seconds: finalSeconds,
};
}
function tsToDayOfWeek(ts: number, tz?: string) {
return new Date(ts).toLocaleString('en', {
timeZone: tz,
weekday: 'short',
});
}
function tsToDateTimeString(ts: number, tz?: string) {
return new Date(ts).toLocaleString('sv', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: tz,
timeZoneName: 'short',
});
}
function formatAbsoluteRange(range: AbsoluteTimeRange, tz?: string) {
return {
fr: `${tsToDayOfWeek(range.from, tz)} | ${tsToDateTimeString(range.from, tz)}`.replaceAll('', '-'),
to: `${tsToDayOfWeek(range.to, tz)} | ${tsToDateTimeString(range.to, tz)}`.replaceAll('', '-'),
};
}
describe('timeRegions', () => {
describe('day of week', () => {
it('returns regions with 4 Mondays in March 2023', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
};
const tr: TimeRange = {
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
},
};
const regions = calculateTimesWithin(cfg, tr);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-03-06 00:00:00 GMT-6',
to: 'Tue | 2023-03-07 00:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 00:00:00 GMT-5',
to: 'Tue | 2023-03-14 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 00:00:00 GMT-5',
to: 'Tue | 2023-03-21 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 00:00:00 GMT-5',
to: 'Tue | 2023-03-28 00:00:00 GMT-5',
},
]);
});
});
describe('day and time of week', () => {
it('returns regions with 4 Mondays at 20:00 in March 2023', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
from: '20:00',
};
const tr: TimeRange = {
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
},
};
const regions = calculateTimesWithin(cfg, tr);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-03-06 20:00:00 GMT-6',
to: 'Mon | 2023-03-06 20:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 20:00:00 GMT-5',
to: 'Mon | 2023-03-13 20:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 20:00:00 GMT-5',
to: 'Mon | 2023-03-20 20:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 20:00:00 GMT-5',
to: 'Mon | 2023-03-27 20:00:00 GMT-5',
},
]);
});
});
describe('day of week range', () => {
it('returns regions with days range', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
toDayOfWeek: 3,
};
const tr: TimeRange = {
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
},
};
const regions = calculateTimesWithin(cfg, tr);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-02-27 00:00:00 GMT-6',
to: 'Thu | 2023-03-02 00:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-06 00:00:00 GMT-6',
to: 'Thu | 2023-03-09 00:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 00:00:00 GMT-5',
to: 'Thu | 2023-03-16 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 00:00:00 GMT-5',
to: 'Thu | 2023-03-23 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 00:00:00 GMT-5',
to: 'Thu | 2023-03-30 00:00:00 GMT-5',
},
]);
});
it('returns regions with days range (browser time zone)', () => {
const dashboardTz = process.env.TZ;
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
toDayOfWeek: 3,
};
const tr: TimeRange = {
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
},
};
const regions = calculateTimesWithin(cfg, tr);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-02-27 00:00:00 GMT-5',
to: 'Thu | 2023-03-02 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-06 00:00:00 GMT-5',
to: 'Thu | 2023-03-09 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-13 00:00:00 GMT-5',
to: 'Thu | 2023-03-16 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 00:00:00 GMT-5',
to: 'Thu | 2023-03-23 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 00:00:00 GMT-5',
to: 'Thu | 2023-03-30 00:00:00 GMT-5',
},
]);
});
it('returns regions with days/times range', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
from: '20:00',
toDayOfWeek: 2,
to: '10:00',
};
const tr: TimeRange = {
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
},
};
const regions = calculateTimesWithin(cfg, tr);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-03-06 20:00:00 GMT-6',
to: 'Tue | 2023-03-07 10:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 20:00:00 GMT-5',
to: 'Tue | 2023-03-14 10:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 20:00:00 GMT-5',
to: 'Tue | 2023-03-21 10:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 20:00:00 GMT-5',
to: 'Tue | 2023-03-28 10:00:00 GMT-5',
},
]);
});
});
type TestDef = [
name: string,
fromDayOfWeek: number | null,
from: string | null,
toDayOfWeek: number | null,
to: string | null,
cronExpr: string,
duration: string,
];
let _ = null;
describe('various scenarios (regions)', () => {
/* eslint-disable */
// prettier-ignore
let tests: TestDef[] = [
['from every day (time before) to every day (time after)', _, '10:27', _, '14:27', '27 10 * * *', '4h'],
['from every day (time after) to every day (time before)', _, '22:27', _, '02:27', '27 22 * * *', '4h'],
['from every day (time) to every day (no time)', _, '10:27', _, _, '27 10 * * *', ''],
['from fri (no time)', 5, _, _, _, '0 0 * * 5', '1d'],
['from fri (no time) to tues (no time)', 5, _, 2, _, '0 0 * * 5', '5d'],
['from fri (no time) to tues (time)', 5, _, 2, '02:27', '0 0 * * 5', '4d 2h 27m'],
['from fri (time) to tues (no time)', 5, '10:27', 2, _, '27 10 * * 5', '4d'],
['from fri (time) to tues (time)', 5, '10:27', 2, '14:27', '27 10 * * 5', '4d 4h'],
// same day
['from fri (time before) to fri (time after)', 5, '10:27', 5, '14:27', '27 10 * * 5', '4h'],
// "toDay" should assume Fri
['from fri (time before) to every day (time after)', 5, '10:27', _, '14:27', '27 10 * * 5', '4h'],
// wrap-around case
['from fri (time after) to fri (time before)', 5, '14:27', 5, '10:27', '27 14 * * 5', '6d 20h'],
];
/* eslint-enable */
tests.forEach(([name, fromDayOfWeek, from, toDayOfWeek, to, cronExpr, duration]) => {
it(name, () => {
const cron = convertToCron(fromDayOfWeek, from, toDayOfWeek, to);
expect(cron).not.toBeUndefined();
expect(cron?.cronExpr).toEqual(cronExpr);
expect(reverseParseDuration(durationFromSeconds(cron?.duration ?? 0), false)).toEqual(duration);
});
});
});
describe('various scenarios (points)', () => {
/* eslint-disable */
// prettier-ignore
let tests: TestDef[] = [
['from every day (time)', _, '10:03', _, _, '3 10 * * *', ''],
['from every day (time) to every day (time)', _, '10:03', _, '10:03', '3 10 * * *', ''],
['from tues (time)', 2, '10:03', _, _, '3 10 * * 2', ''],
['from tues (time) to tues (time)', 2, '10:03', _, '10:03', '3 10 * * 2', ''],
];
/* eslint-enable */
tests.forEach(([name, fromDayOfWeek, from, toDayOfWeek, to, cronExpr, duration]) => {
it(name, () => {
const cron = convertToCron(fromDayOfWeek, from, toDayOfWeek, to);
expect(cron).not.toBeUndefined();
expect(cron?.cronExpr).toEqual(cronExpr);
expect(reverseParseDuration(durationFromSeconds(cron?.duration ?? 0), false)).toEqual(duration);
});
});
});
describe('convert simple time region config to cron string and duration', () => {
it.each`
from | fromDOW | to | toDOW | timezone | expectedCron | expectedDuration
${'03:03'} | ${1} | ${'03:03'} | ${2} | ${'browser'} | ${'3 3 * * 1'} | ${'1d'}
${'03:03'} | ${7} | ${'03:03'} | ${1} | ${'browser'} | ${'3 3 * * 7'} | ${'1d'}
${'09:03'} | ${7} | ${'03:03'} | ${1} | ${'browser'} | ${'3 9 * * 7'} | ${'18h'}
${'03:03'} | ${7} | ${'04:03'} | ${7} | ${'browser'} | ${'3 3 * * 7'} | ${'1h'}
${'03:03'} | ${7} | ${'02:03'} | ${7} | ${'browser'} | ${'3 3 * * 7'} | ${'6d 23h'}
${'03:03'} | ${7} | ${'3:03'} | ${7} | ${'browser'} | ${'3 3 * * 7'} | ${''}
`(
"time region config with from time '$from' and DOW '$fromDOW', to: '$to' and DOW '$toDOW' should generate a cron string of '$expectedCron' and '$expectedDuration'",
({ from, fromDOW, to, toDOW, timezone, expectedCron, expectedDuration }) => {
const timeConfig: TimeRegionConfig = { from, fromDayOfWeek: fromDOW, to, toDayOfWeek: toDOW, timezone };
const convertedCron = convertToCron(
timeConfig.fromDayOfWeek,
timeConfig.from,
timeConfig.toDayOfWeek,
timeConfig.to
)!;
expect(convertedCron).not.toBeUndefined();
expect(convertedCron.cronExpr).toEqual(expectedCron);
expect(reverseParseDuration(durationFromSeconds(convertedCron.duration), false)).toEqual(expectedDuration);
}
);
});
});