Збір конфігів перестав залежати від того, чи згадає людина натиснути кнопку. Свій парсер cron (internal/cronx) замість залежності: бітові маски uint64 на поле, пошук наступного запуску покроково по хвилинах із запобіжником у чотири роки. Правило dom/dow — об'єднання, як у справжнього cron. Неможливий розклад (30 лютого) чесно відмовляє замість зациклення. Планувальник тікає раз на хвилину під advisory-блокуванням, тож у кластері розклад розкручує рівно один екземпляр. Спершу переноситься next_backup_at, потім ставиться завдання: падіння між кроками коштує одного пропущеного бекапу, зворотний порядок дав би нескінченну чергу. Форма розкладу: чотири пресети плюс довільний cron. Виправлено помилку, через яку пункт «свій розклад…» нічого не робив — обробник select відсікав порожнє значення, тобто саме той випадок, заради якого пункт існує. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
4.5 KiB
Go
146 lines
4.5 KiB
Go
package cronx
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func at(t *testing.T, s string) time.Time {
|
||
t.Helper()
|
||
v, err := time.Parse("2006-01-02 15:04", s)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return v
|
||
}
|
||
|
||
func next(t *testing.T, expr, from string) string {
|
||
t.Helper()
|
||
s, err := Parse(expr)
|
||
if err != nil {
|
||
t.Fatalf("Parse(%q): %v", expr, err)
|
||
}
|
||
n, err := s.Next(at(t, from))
|
||
if err != nil {
|
||
t.Fatalf("Next: %v", err)
|
||
}
|
||
return n.Format("2006-01-02 15:04")
|
||
}
|
||
|
||
func TestDailyAtThree(t *testing.T) {
|
||
if got := next(t, "0 3 * * *", "2026-08-16 10:00"); got != "2026-08-17 03:00" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
// Якщо ще не настало сьогодні — сьогодні ж.
|
||
if got := next(t, "0 3 * * *", "2026-08-16 01:30"); got != "2026-08-16 03:00" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
}
|
||
|
||
func TestEveryFifteenMinutes(t *testing.T) {
|
||
if got := next(t, "*/15 * * * *", "2026-08-16 10:07"); got != "2026-08-16 10:15" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
// Рівно на межі — наступний, а не той самий.
|
||
if got := next(t, "*/15 * * * *", "2026-08-16 10:15"); got != "2026-08-16 10:30" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
}
|
||
|
||
func TestListAndRange(t *testing.T) {
|
||
if got := next(t, "0 9,18 * * *", "2026-08-16 10:00"); got != "2026-08-16 18:00" {
|
||
t.Fatalf("список годин: %s", got)
|
||
}
|
||
if got := next(t, "30 8-10 * * *", "2026-08-16 09:45"); got != "2026-08-16 10:30" {
|
||
t.Fatalf("діапазон: %s", got)
|
||
}
|
||
}
|
||
|
||
// 2026-08-16 — неділя; найближчий понеділок 17-го.
|
||
func TestDayOfWeek(t *testing.T) {
|
||
if got := next(t, "0 2 * * 1", "2026-08-16 12:00"); got != "2026-08-17 02:00" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
if got := next(t, "0 2 * * mon", "2026-08-16 12:00"); got != "2026-08-17 02:00" {
|
||
t.Fatalf("назва дня: %s", got)
|
||
}
|
||
// Неділя як 7 — поширений запис.
|
||
if got := next(t, "0 2 * * 7", "2026-08-17 12:00"); got != "2026-08-23 02:00" {
|
||
t.Fatalf("неділя як 7: %s", got)
|
||
}
|
||
}
|
||
|
||
func TestDayOfMonth(t *testing.T) {
|
||
if got := next(t, "0 4 1 * *", "2026-08-16 12:00"); got != "2026-09-01 04:00" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
}
|
||
|
||
// Правило cron: якщо задані і день місяця, і день тижня, підходить
|
||
// збіг за будь-яким. «Щоп'ятниці та 1-го числа» інакше не записати.
|
||
func TestDomAndDowAreUnion(t *testing.T) {
|
||
s, err := Parse("0 5 1 * fri")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 2026-08-21 — п'ятниця.
|
||
n, _ := s.Next(at(t, "2026-08-20 12:00"))
|
||
if got := n.Format("2006-01-02"); got != "2026-08-21" {
|
||
t.Fatalf("п'ятниця: %s", got)
|
||
}
|
||
// 2026-09-01 — вівторок, але перше число.
|
||
n, _ = s.Next(at(t, "2026-08-29 12:00"))
|
||
if got := n.Format("2006-01-02"); got != "2026-09-01" {
|
||
t.Fatalf("перше число: %s", got)
|
||
}
|
||
}
|
||
|
||
func TestMonthNames(t *testing.T) {
|
||
if got := next(t, "0 0 1 jan *", "2026-08-16 12:00"); got != "2027-01-01 00:00" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
}
|
||
|
||
// 29 лютого існує не щороку — перебір має це переживати, а не зациклитись.
|
||
func TestLeapDay(t *testing.T) {
|
||
if got := next(t, "0 0 29 2 *", "2026-03-01 00:00"); got != "2028-02-29 00:00" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
}
|
||
|
||
func TestStepInRange(t *testing.T) {
|
||
if got := next(t, "0 8-20/4 * * *", "2026-08-16 09:00"); got != "2026-08-16 12:00" {
|
||
t.Fatalf("отримали %s", got)
|
||
}
|
||
}
|
||
|
||
func TestBadExpressions(t *testing.T) {
|
||
bad := []string{
|
||
"", "0 3 * *", "0 3 * * * *",
|
||
"60 3 * * *", "0 24 * * *", "0 3 0 * *", "0 3 * 13 *",
|
||
"0 3 * * 8", "x 3 * * *", "*/0 * * * *", "10-5 * * * *",
|
||
}
|
||
for _, e := range bad {
|
||
if _, err := Parse(e); err == nil {
|
||
t.Errorf("%q мав дати помилку", e)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Розклад, який ніколи не настане, має чесно відмовити, а не зависнути.
|
||
func TestImpossibleScheduleFails(t *testing.T) {
|
||
s, err := Parse("0 0 30 2 *") // 30 лютого не буває
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := s.Next(at(t, "2026-01-01 00:00")); err == nil {
|
||
t.Fatal("очікували відмову")
|
||
}
|
||
}
|
||
|
||
// Типовий розклад із seed має розбиратись.
|
||
func TestDefaultPolicyExpression(t *testing.T) {
|
||
if _, err := Parse("0 3 * * *"); err != nil {
|
||
t.Fatalf("типовий розклад не розбирається: %v", err)
|
||
}
|
||
}
|