Netpulse_SasS/server/internal/cronx/cron_test.go
byrsapty 436eff343d Етап 7: планувальник бекапів за cron
Збір конфігів перестав залежати від того, чи згадає людина натиснути
кнопку.

Свій парсер cron (internal/cronx) замість залежності: бітові маски
uint64 на поле, пошук наступного запуску покроково по хвилинах із
запобіжником у чотири роки. Правило dom/dow — об'єднання, як у справжнього
cron. Неможливий розклад (30 лютого) чесно відмовляє замість зациклення.

Планувальник тікає раз на хвилину під advisory-блокуванням, тож у
кластері розклад розкручує рівно один екземпляр. Спершу переноситься
next_backup_at, потім ставиться завдання: падіння між кроками коштує
одного пропущеного бекапу, зворотний порядок дав би нескінченну чергу.

Форма розкладу: чотири пресети плюс довільний cron. Виправлено помилку,
через яку пункт «свій розклад…» нічого не робив — обробник select
відсікав порожнє значення, тобто саме той випадок, заради якого пункт
існує.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:54:07 +03:00

146 lines
4.5 KiB
Go
Raw Permalink 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.

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)
}
}